This is an automated email from the ASF dual-hosted git repository.

exceptionfactory 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 13bb0643f45 NIFI-15299 Improved logic for disabling invalid Controller 
Services (#10630)
13bb0643f45 is described below

commit 13bb0643f45757152f125ba06a8f1f14cbacd802
Author: Pierre Villard <[email protected]>
AuthorDate: Mon Dec 15 18:11:11 2025 +0100

    NIFI-15299 Improved logic for disabling invalid Controller Services (#10630)
    
    Signed-off-by: David Handermann <[email protected]>
---
 .../service/StandardControllerServiceNode.java     |  12 ++
 .../service/StandardControllerServiceProvider.java |  34 +++++-
 .../StandardControllerServiceReference.java        |  27 ++--
 .../TestStandardControllerServiceProvider.java     | 107 ++++++++++++++++
 .../web/dao/impl/StandardControllerServiceDAO.java |  45 ++++++-
 .../ControllerServiceDisableWithReferencesIT.java  | 136 +++++++++++++++++++++
 6 files changed, 343 insertions(+), 18 deletions(-)

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 18202bc10dd..5d81e96ba4b 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
@@ -753,6 +753,18 @@ public class StandardControllerServiceNode extends 
AbstractComponentNode impleme
         final CompletableFuture<Void> future = new CompletableFuture<>();
         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();
+
+                // 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;
         }
 
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceProvider.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceProvider.java
index 714a5e0bd2b..f1945c8ed55 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceProvider.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceProvider.java
@@ -192,14 +192,18 @@ public class StandardControllerServiceProvider implements 
ControllerServiceProvi
 
         final Map<ComponentNode, Future<Void>> updated = new HashMap<>();
 
-        // verify that  we can stop all components (that are running) before 
doing anything
+        // verify that we can stop all components (that are running or 
starting) before doing anything
+        // Note: We check both RUNNING and STARTING states because a processor 
might be stuck in STARTING
+        // state if it references an invalid controller service (e.g., after a 
restart when the controller
+        // service configuration became invalid). Such processors need to be 
stopped before the controller
+        // service can be disabled.
         for (final ProcessorNode node : processors) {
-            if (node.getScheduledState() == ScheduledState.RUNNING) {
+            if (isRunningOrStarting(node)) {
                 node.verifyCanStop();
             }
         }
         for (final ReportingTaskNode node : reportingTasks) {
-            if (node.getScheduledState() == ScheduledState.RUNNING) {
+            if (isRunningOrStarting(node)) {
                 node.verifyCanStop();
             }
         }
@@ -209,15 +213,15 @@ public class StandardControllerServiceProvider implements 
ControllerServiceProvi
             }
         }
 
-        // stop all of the components that are running
+        // stop all of the components that are running or starting
         for (final ProcessorNode node : processors) {
-            if (node.getScheduledState() == ScheduledState.RUNNING) {
+            if (isRunningOrStarting(node)) {
                 final Future<Void> future = 
node.getProcessGroup().stopProcessor(node);
                 updated.put(node, future);
             }
         }
         for (final ReportingTaskNode node : reportingTasks) {
-            if (node.getScheduledState() == ScheduledState.RUNNING) {
+            if (isRunningOrStarting(node)) {
                 final Future<Void> future = processScheduler.unschedule(node);
                 updated.put(node, future);
             }
@@ -240,6 +244,24 @@ public class StandardControllerServiceProvider implements 
ControllerServiceProvi
         return updated;
     }
 
+    /**
+     * Checks if a processor is running or in the process of starting.
+     * A processor might be stuck in STARTING state if it references an 
invalid controller service.
+     */
+    private boolean isRunningOrStarting(final ProcessorNode node) {
+        final ScheduledState physicalState = node.getPhysicalScheduledState();
+        return physicalState == ScheduledState.RUNNING || physicalState == 
ScheduledState.STARTING;
+    }
+
+    /**
+     * Checks if a reporting task is running or in the process of starting.
+     * A reporting task might be stuck in STARTING state if it references an 
invalid controller service.
+     */
+    private boolean isRunningOrStarting(final ReportingTaskNode node) {
+        final ScheduledState scheduledState = node.getScheduledState();
+        return scheduledState == ScheduledState.RUNNING || scheduledState == 
ScheduledState.STARTING;
+    }
+
     @Override
     public CompletableFuture<Void> enableControllerService(final 
ControllerServiceNode serviceNode) {
         if (serviceNode.isActive()) {
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceReference.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceReference.java
index cb92b2ba841..43b2802779e 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceReference.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceReference.java
@@ -25,6 +25,7 @@ import java.util.Set;
 import org.apache.nifi.controller.ComponentNode;
 import org.apache.nifi.controller.ProcessorNode;
 import org.apache.nifi.controller.ReportingTaskNode;
+import org.apache.nifi.controller.ScheduledState;
 
 public class StandardControllerServiceReference implements 
ControllerServiceReference {
 
@@ -46,17 +47,25 @@ public class StandardControllerServiceReference implements 
ControllerServiceRefe
         return Collections.unmodifiableSet(components);
     }
 
-    private boolean isRunning(final ComponentNode component) {
-        if (component instanceof ReportingTaskNode) {
-            return ((ReportingTaskNode) component).isRunning();
+    /**
+     * Determines if a component is running or in the process of starting.
+     * A component might be stuck in STARTING state if it references an 
invalid controller service
+     * (e.g., after a restart when the controller service configuration became 
invalid).
+     * Such components are considered "active" and would prevent the 
controller service from being disabled.
+     */
+    private boolean isActive(final ComponentNode component) {
+        if (component instanceof ReportingTaskNode reportingTaskNode) {
+            final ScheduledState state = reportingTaskNode.getScheduledState();
+            return state == ScheduledState.RUNNING || state == 
ScheduledState.STARTING;
         }
 
-        if (component instanceof ProcessorNode) {
-            return ((ProcessorNode) component).isRunning();
+        if (component instanceof ProcessorNode processorNode) {
+            final ScheduledState state = 
processorNode.getPhysicalScheduledState();
+            return state == ScheduledState.RUNNING || state == 
ScheduledState.STARTING;
         }
 
-        if (component instanceof ControllerServiceNode) {
-            return ((ControllerServiceNode) component).isActive();
+        if (component instanceof ControllerServiceNode controllerServiceNode) {
+            return controllerServiceNode.isActive();
         }
 
         return false;
@@ -67,13 +76,13 @@ public class StandardControllerServiceReference implements 
ControllerServiceRefe
         final Set<ComponentNode> activeReferences = new HashSet<>();
 
         for (final ComponentNode component : components) {
-            if (isRunning(component)) {
+            if (isActive(component)) {
                 activeReferences.add(component);
             }
         }
 
         for (final ComponentNode component : 
findRecursiveReferences(ComponentNode.class)) {
-            if (isRunning(component)) {
+            if (isActive(component)) {
                 activeReferences.add(component);
             }
         }
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 1223df34caa..f71f4ed2ed8 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
@@ -20,6 +20,7 @@ package org.apache.nifi.controller.service;
 import org.apache.nifi.bundle.Bundle;
 import org.apache.nifi.bundle.BundleCoordinate;
 import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.controller.ComponentNode;
 import org.apache.nifi.components.state.Scope;
 import org.apache.nifi.components.state.StateManager;
 import org.apache.nifi.components.state.StateManagerProvider;
@@ -70,6 +71,7 @@ import java.util.LinkedHashMap;
 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.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
@@ -82,6 +84,10 @@ 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.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 public class TestStandardControllerServiceProvider {
 
@@ -437,6 +443,107 @@ public class TestStandardControllerServiceProvider {
         assertEquals(ScheduledState.STOPPED, procNode.getScheduledState());
     }
 
+    /**
+     * Test that unscheduleReferencingComponents handles processors in 
STARTING state.
+     * This scenario can occur when a processor references an invalid 
controller service
+     * (e.g., after a restart when the controller service configuration became 
invalid).
+     * The processor might be stuck in STARTING state and should still be 
stopped.
+     */
+    @Test
+    public void 
testUnscheduleReferencingComponentsIncludesStartingProcessors() {
+        final ProcessGroup procGroup = new MockProcessGroup(flowManager);
+
+        final FlowManager flowManager = mock(FlowManager.class);
+        when(flowManager.getGroup(anyString())).thenReturn(procGroup);
+
+        final StandardProcessScheduler scheduler = createScheduler();
+        final StandardControllerServiceProvider provider = new 
StandardControllerServiceProvider(scheduler, null, flowManager, 
extensionManager);
+        final ControllerServiceNode serviceNode = 
createControllerService(ServiceA.class.getName(), "1", 
systemBundle.getBundleDetails().getCoordinate(), provider);
+
+        // Create a mock processor that is in STARTING state
+        final ProcessorNode mockProcNode = mock(ProcessorNode.class);
+        
when(mockProcNode.getPhysicalScheduledState()).thenReturn(ScheduledState.STARTING);
+        when(mockProcNode.isRunning()).thenReturn(false); // isRunning() 
returns false for STARTING
+        
when(mockProcNode.getScheduledState()).thenReturn(ScheduledState.RUNNING);
+
+        // Mock the process group and stop processor behavior
+        final ProcessGroup mockProcessGroup = mock(ProcessGroup.class);
+        when(mockProcNode.getProcessGroup()).thenReturn(mockProcessGroup);
+        
when(mockProcessGroup.stopProcessor(mockProcNode)).thenReturn(CompletableFuture.completedFuture(null));
+
+        serviceNode.addReference(mockProcNode, 
PropertyDescriptor.NULL_DESCRIPTOR);
+
+        // The unscheduleReferencingComponents should include the processor in 
STARTING state
+        // This verifies that the method checks for both RUNNING and STARTING 
states
+        provider.unscheduleReferencingComponents(serviceNode);
+
+        // Verify that verifyCanStop was called on the processor (indicating 
it was considered for stopping)
+        verify(mockProcNode).verifyCanStop();
+        // Verify that stopProcessor was called
+        verify(mockProcessGroup).stopProcessor(mockProcNode);
+    }
+
+    /**
+     * Test that getActiveReferences in StandardControllerServiceReference 
considers
+     * processors in STARTING state as active (in addition to RUNNING).
+     */
+    @Test
+    public void testGetActiveReferencesIncludesStartingProcessors() {
+        final ProcessGroup procGroup = new MockProcessGroup(flowManager);
+
+        final FlowManager flowManager = mock(FlowManager.class);
+        when(flowManager.getGroup(anyString())).thenReturn(procGroup);
+
+        final StandardProcessScheduler scheduler = createScheduler();
+        final StandardControllerServiceProvider provider = new 
StandardControllerServiceProvider(scheduler, null, flowManager, 
extensionManager);
+        final ControllerServiceNode serviceNode = 
createControllerService(ServiceA.class.getName(), "1", 
systemBundle.getBundleDetails().getCoordinate(), provider);
+
+        // Create a mock processor that is in STARTING state
+        final ProcessorNode mockProcNode = mock(ProcessorNode.class);
+        
when(mockProcNode.getPhysicalScheduledState()).thenReturn(ScheduledState.STARTING);
+        when(mockProcNode.isRunning()).thenReturn(false); // isRunning() 
returns false for STARTING
+        
when(mockProcNode.getScheduledState()).thenReturn(ScheduledState.RUNNING);
+
+        serviceNode.addReference(mockProcNode, 
PropertyDescriptor.NULL_DESCRIPTOR);
+
+        // Get active references - should include the STARTING processor
+        final Set<ComponentNode> activeReferences = 
serviceNode.getReferences().getActiveReferences();
+
+        // The STARTING processor should be considered active
+        assertTrue(activeReferences.contains(mockProcNode),
+            "Processor in STARTING state should be considered an active 
reference");
+    }
+
+    /**
+     * Test that getActiveReferences does not include processors in STOPPED 
state.
+     */
+    @Test
+    public void testGetActiveReferencesExcludesStoppedProcessors() {
+        final ProcessGroup procGroup = new MockProcessGroup(flowManager);
+
+        final FlowManager flowManager = mock(FlowManager.class);
+        when(flowManager.getGroup(anyString())).thenReturn(procGroup);
+
+        final StandardProcessScheduler scheduler = createScheduler();
+        final StandardControllerServiceProvider provider = new 
StandardControllerServiceProvider(scheduler, null, flowManager, 
extensionManager);
+        final ControllerServiceNode serviceNode = 
createControllerService(ServiceA.class.getName(), "1", 
systemBundle.getBundleDetails().getCoordinate(), provider);
+
+        // Create a mock processor that is in STOPPED state
+        final ProcessorNode mockProcNode = mock(ProcessorNode.class);
+        
when(mockProcNode.getPhysicalScheduledState()).thenReturn(ScheduledState.STOPPED);
+        when(mockProcNode.isRunning()).thenReturn(false);
+        
when(mockProcNode.getScheduledState()).thenReturn(ScheduledState.STOPPED);
+
+        serviceNode.addReference(mockProcNode, 
PropertyDescriptor.NULL_DESCRIPTOR);
+
+        // Get active references - should NOT include the STOPPED processor
+        final Set<ComponentNode> activeReferences = 
serviceNode.getReferences().getActiveReferences();
+
+        // The STOPPED processor should NOT be considered active
+        assertFalse(activeReferences.contains(mockProcNode),
+            "Processor in STOPPED state should not be considered an active 
reference");
+    }
+
     @Test
     public void validateEnableServices() {
         final FlowManager flowManager = Mockito.mock(FlowManager.class);
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardControllerServiceDAO.java
 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardControllerServiceDAO.java
index c89ac1b0f39..d7d4d51a66e 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardControllerServiceDAO.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardControllerServiceDAO.java
@@ -59,6 +59,8 @@ import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
 @Repository
@@ -186,7 +188,7 @@ public class StandardControllerServiceDAO extends 
ComponentDAO implements Contro
                 if 
(ControllerServiceState.ENABLED.equals(purposedControllerServiceState)) {
                     serviceProvider.enableControllerService(controllerService);
                 } else if 
(ControllerServiceState.DISABLED.equals(purposedControllerServiceState)) {
-                    
serviceProvider.disableControllerService(controllerService);
+                    disableControllerServiceAndReferences(controllerService);
                 }
             }
         }
@@ -212,6 +214,40 @@ public class StandardControllerServiceDAO extends 
ComponentDAO implements Contro
         return controllerService;
     }
 
+    /**
+     * Disables a Controller Service along with all its referencing components.
+     * This method handles the complete disable workflow:
+     * 1. Stops all referencing schedulable components (processors, reporting 
tasks, etc.)
+     * 2. Waits for all referencing components to stop
+     * 3. Disables all referencing controller services
+     * 4. Verifies the controller service can be disabled
+     * 5. Disables the controller service itself
+     *
+     * @param controllerService the controller service to disable
+     */
+    private void disableControllerServiceAndReferences(final 
ControllerServiceNode controllerService) {
+        // First, unschedule all referencing schedulable components 
(processors, reporting tasks, etc.)
+        final Map<ComponentNode, Future<Void>> unscheduleFutures = 
serviceProvider.unscheduleReferencingComponents(controllerService);
+
+        // Wait for all referencing components to stop
+        for (final Map.Entry<ComponentNode, Future<Void>> entry : 
unscheduleFutures.entrySet()) {
+            try {
+                entry.getValue().get(30, TimeUnit.SECONDS);
+            } catch (final Exception e) {
+                throw new NiFiCoreException("Failed to stop referencing 
component " + entry.getKey().getIdentifier(), e);
+            }
+        }
+
+        // Next, disable all referencing controller services
+        serviceProvider.disableReferencingServices(controllerService);
+
+        // Verify that all referencing components are now stopped before 
disabling
+        controllerService.verifyCanDisable();
+
+        // Finally, disable the controller service itself
+        serviceProvider.disableControllerService(controllerService);
+    }
+
     private void updateBundle(final ControllerServiceNode controllerService, 
final ControllerServiceDTO controllerServiceDTO) {
         final BundleDTO bundleDTO = controllerServiceDTO.getBundle();
         if (bundleDTO != null) {
@@ -328,9 +364,12 @@ public class StandardControllerServiceDAO extends 
ComponentDAO implements Contro
 
                     if 
(ControllerServiceState.ENABLED.equals(purposedControllerServiceState)) {
                         controllerService.verifyCanEnable();
-                    } else if 
(ControllerServiceState.DISABLED.equals(purposedControllerServiceState)) {
-                        controllerService.verifyCanDisable();
                     }
+                    // Note: We don't call verifyCanDisable() here because we 
will automatically
+                    // stop referencing components (processors, reporting 
tasks) and disable
+                    // referencing controller services before disabling this 
service.
+                    // The verifyCanDisable() check is performed in 
updateControllerService()
+                    // AFTER the referencing components have been stopped.
                 }
             } catch (final IllegalArgumentException iae) {
                 throw new IllegalArgumentException("Controller Service state: 
Value must be one of [ENABLED, DISABLED]");
diff --git 
a/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/controllerservice/ControllerServiceDisableWithReferencesIT.java
 
b/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/controllerservice/ControllerServiceDisableWithReferencesIT.java
new file mode 100644
index 00000000000..15a7f2f77c9
--- /dev/null
+++ 
b/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/controllerservice/ControllerServiceDisableWithReferencesIT.java
@@ -0,0 +1,136 @@
+/*
+ * 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.tests.system.controllerservice;
+
+import org.apache.nifi.tests.system.NiFiSystemIT;
+import org.apache.nifi.toolkit.client.NiFiClientException;
+import org.apache.nifi.web.api.dto.ProcessorDTO;
+import org.apache.nifi.web.api.entity.ControllerServiceEntity;
+import org.apache.nifi.web.api.entity.ProcessorEntity;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * System tests for verifying that disabling a Controller Service properly 
stops
+ * referencing components (processors, reporting tasks, etc.) before disabling.
+ *
+ * This addresses the scenario where:
+ * 1. A Controller Service is enabled with referencing processors running
+ * 2. User attempts to disable the Controller Service
+ * 3. The system should automatically stop referencing processors first
+ * 4. Then disable the Controller Service
+ */
+public class ControllerServiceDisableWithReferencesIT extends NiFiSystemIT {
+
+    private static final String DISABLED = "DISABLED";
+    private static final String ENABLING = "ENABLING";
+    private static final String RUNNING = "RUNNING";
+    private static final String STOPPED = "STOPPED";
+
+    /**
+     * Test that disabling a Controller Service automatically stops 
referencing processors.
+     * This verifies the fix for the issue where disabling a Controller 
Service would fail
+     * if there were running processors that referenced it.
+     */
+    @Test
+    public void testDisableControllerServiceStopsReferencingProcessors() 
throws NiFiClientException, IOException, InterruptedException {
+        // Create a simple controller service (StandardSleepService doesn't 
require external resources)
+        final ControllerServiceEntity sleepService = 
getClientUtil().createControllerService("StandardSleepService");
+
+        // Create a processor that references this controller service
+        final ProcessorEntity processor = 
getClientUtil().createProcessor("Sleep");
+        getClientUtil().updateProcessorProperties(processor, Map.of("Sleep 
Service", sleepService.getId()));
+        getClientUtil().setAutoTerminatedRelationships(processor, "success");
+
+        // Enable the controller service
+        getClientUtil().enableControllerService(sleepService);
+        getClientUtil().waitForControllerServicesEnabled("root");
+
+        // Start the processor
+        getClientUtil().waitForValidProcessor(processor.getId());
+        getClientUtil().startProcessor(processor);
+        getClientUtil().waitForProcessorState(processor.getId(), RUNNING);
+
+        // Now disable the controller service - this should automatically stop 
the processor first
+        final ControllerServiceEntity serviceToDisable = 
getNifiClient().getControllerServicesClient().getControllerService(sleepService.getId());
+        getClientUtil().disableControllerService(serviceToDisable);
+
+        // Wait for the controller service to be disabled
+        
getClientUtil().waitForControllerServiceRunStatus(sleepService.getId(), 
DISABLED);
+
+        // Verify the processor was stopped
+        final ProcessorEntity updatedProcessor = 
getNifiClient().getProcessorClient().getProcessor(processor.getId());
+        final ProcessorDTO processorDto = updatedProcessor.getComponent();
+        assertEquals(STOPPED, processorDto.getState(),
+            "Processor should be stopped after disabling the referenced 
controller service");
+
+        // Verify the controller service is disabled
+        final ControllerServiceEntity updatedService = 
getNifiClient().getControllerServicesClient().getControllerService(sleepService.getId());
+        assertEquals(DISABLED, updatedService.getComponent().getState(),
+            "Controller Service should be disabled");
+    }
+
+    /**
+     * Test that disabling a Controller Service works when the controller 
service
+     * is stuck in ENABLING state (e.g., due to validation failures).
+     * This verifies the fix for the issue where disabling took too long when 
the
+     * service was stuck in ENABLING state.
+     */
+    @Test
+    public void testDisableControllerServiceInEnablingState() throws 
NiFiClientException, IOException, InterruptedException {
+        // Create a LifecycleFailureService configured to fail many times 
(effectively stuck in ENABLING)
+        final ControllerServiceEntity failureService = 
getClientUtil().createControllerService("LifecycleFailureService");
+        getClientUtil().updateControllerServiceProperties(failureService, 
Collections.singletonMap("Enable Failure Count", "10000"));
+
+        // Try to enable the service - it will be stuck in ENABLING state
+        getClientUtil().enableControllerService(failureService);
+
+        // Wait a bit for it to be in ENABLING state
+        Thread.sleep(1000);
+
+        // Verify it's in ENABLING state
+        ControllerServiceEntity currentService = 
getNifiClient().getControllerServicesClient().getControllerService(failureService.getId());
+        assertEquals(ENABLING, currentService.getComponent().getState(),
+            "Controller Service should be in ENABLING state");
+
+        // Now disable the service - this should complete quickly (not wait 
for retry delay)
+        final long startTime = System.currentTimeMillis();
+        getClientUtil().disableControllerService(currentService);
+
+        // Wait for the controller service to be disabled
+        
getClientUtil().waitForControllerServiceRunStatus(failureService.getId(), 
DISABLED);
+        final long endTime = System.currentTimeMillis();
+
+        // Verify it completed in a reasonable time (less than 30 seconds)
+        // Previously this could take up to 10 minutes due to retry delays
+        final long durationSeconds = (endTime - startTime) / 1000;
+        assertTrue(durationSeconds < 30,
+            "Disabling controller service from ENABLING state should complete 
quickly, but took " + durationSeconds + " seconds");
+
+        // Verify the controller service is disabled
+        final ControllerServiceEntity updatedService = 
getNifiClient().getControllerServicesClient().getControllerService(failureService.getId());
+        assertEquals(DISABLED, updatedService.getComponent().getState(),
+            "Controller Service should be disabled");
+    }
+}
+

Reply via email to