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");
+ }
+}
+