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 d512716ed9e NIFI-16055: Avoid starting processors and controller
services in Troubleshooting Connectors when autoResumeState=false (#11375)
d512716ed9e is described below
commit d512716ed9e2b0ac766259bcf84498fa777a675e
Author: Mark Payne <[email protected]>
AuthorDate: Tue Jun 30 02:57:33 2026 -0400
NIFI-16055: Avoid starting processors and controller services in
Troubleshooting Connectors when autoResumeState=false (#11375)
---
.../nifi/groups/NonStartingComponentScheduler.java | 76 ++++++++++++++
.../apache/nifi/groups/StandardProcessGroup.java | 11 +-
.../connectors/ClusteredConnectorAutoResumeIT.java | 33 ++++++
.../system/connectors/ConnectorAutoResumeIT.java | 112 +++++++++++++++++++--
4 files changed, 223 insertions(+), 9 deletions(-)
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/groups/NonStartingComponentScheduler.java
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/groups/NonStartingComponentScheduler.java
new file mode 100644
index 00000000000..673e59b3948
--- /dev/null
+++
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/groups/NonStartingComponentScheduler.java
@@ -0,0 +1,76 @@
+/*
+ * 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.groups;
+
+import org.apache.nifi.connectable.Connectable;
+import org.apache.nifi.controller.ReportingTaskNode;
+import org.apache.nifi.controller.service.ControllerServiceNode;
+import org.apache.nifi.controller.service.ControllerServiceProvider;
+import org.apache.nifi.registry.flow.mapping.VersionedComponentStateLookup;
+
+import java.util.Collection;
+
+/**
+ * A {@link ComponentScheduler} that synchronizes a flow's structure while
leaving every component stopped or disabled
+ * rather than resuming the running or enabled state recorded in the proposed
flow. Components that the proposed flow marks
+ * as DISABLED remain DISABLED; components marked ENABLED or RUNNING are left
in a stopped (but enabled) state. No
+ * Processor, Port, Reporting Task, stateless group, or Controller Service is
started or enabled.
+ *
+ * <p>This differs from {@link ComponentScheduler#NOP_SCHEDULER}, which
performs no scheduling action at all: because a
+ * newly created component defaults to a stopped state, the no-op scheduler
silently turns a component that the proposed
+ * flow marks DISABLED into a stopped (enabled) component. This scheduler
still applies the enabled/disabled transition so
+ * that a DISABLED component is faithfully restored as DISABLED, while
suppressing only the start or enable that would
+ * resume a component to a running or active state.</p>
+ *
+ * <p>It is used wherever a flow's components must be synchronized without
resuming their running state, such as restoring
+ * a Connector's managed flow during NiFi startup when {@code
nifi.flowcontroller.autoResumeState} is {@code false}.</p>
+ */
+public class NonStartingComponentScheduler extends AbstractComponentScheduler {
+
+ public NonStartingComponentScheduler(final ControllerServiceProvider
controllerServiceProvider, final VersionedComponentStateLookup stateLookup) {
+ super(controllerServiceProvider, stateLookup);
+ }
+
+ @Override
+ public void startComponent(final Connectable component) {
+ }
+
+ @Override
+ public void startReportingTask(final ReportingTaskNode reportingTask) {
+ }
+
+ @Override
+ public void startStatelessGroup(final ProcessGroup group) {
+ }
+
+ @Override
+ public void enableControllerServicesAsync(final
Collection<ControllerServiceNode> controllerServices) {
+ }
+
+ @Override
+ protected void startNow(final Connectable component) {
+ }
+
+ @Override
+ protected void startNow(final ReportingTaskNode reportingTask) {
+ }
+
+ @Override
+ protected void startNow(final ProcessGroup statelessGroup) {
+ }
+}
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
index c4da90a3537..dc46a099703 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
@@ -218,6 +218,7 @@ public final class StandardProcessGroup implements
ProcessGroup {
private final DataValve dataValve;
private final Long nifiPropertiesBackpressureCount;
private final String nifiPropertiesBackpressureSize;
+ private final boolean autoResumeState;
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
@@ -272,6 +273,8 @@ public final class StandardProcessGroup implements
ProcessGroup {
this.logFileSuffix = null;
// save only the nifi properties needed, and account for the
possibility those properties are missing
+ this.autoResumeState = nifiProperties == null ?
NiFiProperties.DEFAULT_AUTO_RESUME_STATE : nifiProperties.getAutoResumeState();
+
if (nifiProperties == null) {
nifiPropertiesBackpressureCount = DEFAULT_BACKPRESSURE_OBJECT;
nifiPropertiesBackpressureSize = DEFAULT_BACKPRESSURE_DATA_SIZE;
@@ -3989,7 +3992,13 @@ public final class StandardProcessGroup implements
ProcessGroup {
// their Connections upon restore.
final ComponentIdGenerator idGenerator = (proposedId, instanceId,
destinationGroupId) -> instanceId;
final VersionedComponentStateLookup stateLookup =
VersionedComponentStateLookup.IDENTITY_LOOKUP;
- final ComponentScheduler componentScheduler = new
DefaultComponentScheduler(controllerServiceProvider, stateLookup);
+
+ // When nifi.flowcontroller.autoResumeState is false, restore the flow
structure (so queued FlowFiles can be
+ // re-associated with their Connections) without resuming any
component to its previously running or enabled
+ // state. Otherwise, restore each component to the running/enabled
state captured in the persisted flow.
+ final ComponentScheduler componentScheduler = autoResumeState
+ ? new DefaultComponentScheduler(controllerServiceProvider,
stateLookup)
+ : new NonStartingComponentScheduler(controllerServiceProvider,
stateLookup);
final FlowSynchronizationOptions synchronizationOptions = new
FlowSynchronizationOptions.Builder()
.componentIdGenerator(idGenerator)
diff --git
a/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/connectors/ClusteredConnectorAutoResumeIT.java
b/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/connectors/ClusteredConnectorAutoResumeIT.java
new file mode 100644
index 00000000000..338b2f1953d
--- /dev/null
+++
b/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/connectors/ClusteredConnectorAutoResumeIT.java
@@ -0,0 +1,33 @@
+/*
+ * 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.connectors;
+
+import org.apache.nifi.tests.system.NiFiInstanceFactory;
+
+/**
+ * Clustered variant of {@link ConnectorAutoResumeIT}. Re-executes every
auto-resume test defined on the parent class
+ * against a two-node NiFi cluster so that the cluster-replication and
node-restart paths are exercised in addition to
+ * the standalone paths.
+ */
+public class ClusteredConnectorAutoResumeIT extends ConnectorAutoResumeIT {
+
+ @Override
+ public NiFiInstanceFactory getInstanceFactory() {
+ return createTwoNodeInstanceFactory();
+ }
+}
diff --git
a/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/connectors/ConnectorAutoResumeIT.java
b/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/connectors/ConnectorAutoResumeIT.java
index 2f85b4f7b20..a921f445cee 100644
---
a/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/connectors/ConnectorAutoResumeIT.java
+++
b/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/connectors/ConnectorAutoResumeIT.java
@@ -18,21 +18,32 @@
package org.apache.nifi.tests.system.connectors;
import org.apache.nifi.components.connector.ConnectorState;
+import org.apache.nifi.controller.ScheduledState;
import org.apache.nifi.tests.system.NiFiSystemIT;
import org.apache.nifi.toolkit.client.NiFiClientException;
import org.apache.nifi.util.NiFiProperties;
+import org.apache.nifi.web.api.dto.flow.FlowDTO;
import org.apache.nifi.web.api.entity.ConnectorEntity;
+import org.apache.nifi.web.api.entity.ControllerServiceEntity;
+import org.apache.nifi.web.api.entity.ProcessGroupEntity;
+import org.apache.nifi.web.api.entity.ProcessGroupFlowEntity;
+import org.apache.nifi.web.api.entity.ProcessorEntity;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
/**
- * System test verifying that a running Connector is not auto-resumed after a
restart
- * when the {@code nifi.flowcontroller.autoResumeState} property is set to
{@code false}.
+ * System tests verifying that Connectors and the components inside their
managed flows are not
+ * auto-resumed after a restart when the {@code
nifi.flowcontroller.autoResumeState} property is
+ * set to {@code false}.
*/
public class ConnectorAutoResumeIT extends NiFiSystemIT {
@@ -58,19 +69,104 @@ public class ConnectorAutoResumeIT extends NiFiSystemIT {
getClientUtil().startConnector(connectorId);
getClientUtil().waitForConnectorState(connectorId,
ConnectorState.RUNNING);
- logger.info("Connector {} is RUNNING", connectorId);
- getNiFiInstance().stop();
- logger.info("NiFi stopped");
+ restartWithAutoResumeStateDisabled();
+
+ final ConnectorEntity connectorAfterRestart =
getNifiClient().getConnectorClient().getConnector(connectorId);
+ final String stateAfterRestart =
connectorAfterRestart.getComponent().getState();
+
+ assertEquals(ConnectorState.STOPPED.name(), stateAfterRestart);
+ }
+
+ /**
+ * Verifies that when a Connector is in Troubleshooting mode with
processors running and controller services
+ * enabled, restarting NiFi with {@code
nifi.flowcontroller.autoResumeState=false} leaves the Connector in
+ * Troubleshooting mode but does not resume any of the processors or
controller services inside the managed flow.
+ */
+ @Test
+ public void testTroubleshootingConnectorNotResumedWhenAutoResumeDisabled()
throws NiFiClientException, IOException, InterruptedException {
+ final ConnectorEntity connector =
getClientUtil().createConnector("ComponentLifecycleConnector");
+ final String connectorId = connector.getId();
+
+ getClientUtil().applyConnectorUpdate(connector);
+ getClientUtil().waitForValidConnector(connectorId);
+
+ getClientUtil().startConnector(connectorId);
+ getClientUtil().waitForConnectorState(connectorId,
ConnectorState.RUNNING);
+
+ // Entering Troubleshooting from RUNNING leaves the managed flow's
processors running and its controller services
+ // enabled, so the post-restart assertions have running components to
verify against.
+ getClientUtil().enterTroubleshooting(connectorId);
+ getClientUtil().waitForConnectorState(connectorId,
ConnectorState.TROUBLESHOOTING);
+
+ restartWithAutoResumeStateDisabled();
+
+ // The Troubleshooting state itself is preserved across restarts;
autoResumeState does not transition the
+ // Connector out of Troubleshooting mode.
+ final ConnectorEntity connectorAfterRestart =
getNifiClient().getConnectorClient().getConnector(connectorId);
+ final String connectorStateAfterRestart =
connectorAfterRestart.getComponent().getState();
+ assertEquals(ConnectorState.TROUBLESHOOTING.name(),
connectorStateAfterRestart);
+
+ final List<ProcessorEntity> processorsAfterRestart = new ArrayList<>();
+ collectProcessors(connectorId, null, processorsAfterRestart);
+ assertFalse(processorsAfterRestart.isEmpty());
+
+ for (final ProcessorEntity processor : processorsAfterRestart) {
+ final String processorState = processor.getComponent().getState();
+ assertNotEquals(ScheduledState.RUNNING.name(), processorState);
+ }
+
+ final String managedGroupId =
connectorAfterRestart.getComponent().getManagedProcessGroupId();
+ final List<String> controllerServiceIds =
collectAllControllerServiceIds(managedGroupId);
+ assertFalse(controllerServiceIds.isEmpty());
+
+ for (final String serviceId : controllerServiceIds) {
+ final ControllerServiceEntity serviceEntity =
getNifiClient().getControllerServicesClient().getControllerService(serviceId);
+ final String serviceState =
serviceEntity.getComponent().getState();
+ assertNotEquals("ENABLED", serviceState);
+ }
+ }
+ private void restartWithAutoResumeStateDisabled() throws IOException {
+ getNiFiInstance().stop();
getNiFiInstance().setProperty(NiFiProperties.AUTO_RESUME_STATE,
"false");
getNiFiInstance().start();
+
setupClient();
+
+ if (getNiFiInstance().isClustered()) {
+ waitForAllNodesConnected();
+ }
+
logger.info("NiFi restarted with autoResumeState=false");
+ }
- final ConnectorEntity connectorAfterRestart =
getNifiClient().getConnectorClient().getConnector(connectorId);
- final String stateAfterRestart =
connectorAfterRestart.getComponent().getState();
+ private void collectProcessors(final String connectorId, final String
groupId, final List<ProcessorEntity> collected) throws NiFiClientException,
IOException {
+ final ProcessGroupFlowEntity entity = (groupId == null)
+ ? getNifiClient().getConnectorClient().getFlow(connectorId)
+ : getNifiClient().getConnectorClient().getFlow(connectorId,
groupId);
- assertEquals(ConnectorState.STOPPED.name(), stateAfterRestart);
+ final FlowDTO flow = entity.getProcessGroupFlow().getFlow();
+ collected.addAll(flow.getProcessors());
+
+ for (final ProcessGroupEntity child : flow.getProcessGroups()) {
+ collectProcessors(connectorId, child.getId(), collected);
+ }
+ }
+
+ private List<String> collectAllControllerServiceIds(final String groupId)
throws NiFiClientException, IOException {
+ final List<String> serviceIds = new ArrayList<>();
+ for (final ControllerServiceEntity entity :
getNifiClient().getFlowClient().getControllerServices(groupId).getControllerServices())
{
+ if (entity.getComponent() != null) {
+ serviceIds.add(entity.getId());
+ }
+ }
+
+ final ProcessGroupFlowEntity groupFlow =
getNifiClient().getFlowClient().getProcessGroup(groupId);
+ for (final ProcessGroupEntity child :
groupFlow.getProcessGroupFlow().getFlow().getProcessGroups()) {
+ serviceIds.addAll(collectAllControllerServiceIds(child.getId()));
+ }
+
+ return serviceIds;
}
}