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

pvillard 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 dd7ff5a0011 NIFI-15741: When synchronizing flow, delegate Connector 
starts to Flo… (#11063)
dd7ff5a0011 is described below

commit dd7ff5a0011bbe0246f2262a44896ea33aa981c4
Author: Mark Payne <[email protected]>
AuthorDate: Mon Mar 30 16:44:31 2026 -0400

    NIFI-15741: When synchronizing flow, delegate Connector starts to Flo… 
(#11063)
    
    * NIFI-15741: When synchronizing flow, delegate Connector starts to 
FlowController first so that they are not started before FlowController has 
initialized and not at all if autoResumeState=false
    * NIFI-15741: If Connectors are not to be started due to 
autoResumeState=false, explicitly stop them to ensure that the state 
transitions from UPDATED to STOPPED.
---
 .../org/apache/nifi/controller/FlowController.java | 50 ++++++++++++++
 .../serialization/VersionedDataflowMapper.java     |  4 ++
 .../serialization/VersionedFlowSynchronizer.java   | 18 +++--
 .../VersionedFlowSynchronizerTest.java             | 63 +++++++++++++++++-
 .../system/connectors/ConnectorAutoResumeIT.java   | 76 ++++++++++++++++++++++
 5 files changed, 204 insertions(+), 7 deletions(-)

diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
index e92b931bfe8..c3de110791e 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
@@ -345,6 +345,7 @@ public class FlowController implements 
ReportingTaskProvider, FlowAnalysisRulePr
     private final Set<Connectable> startConnectablesAfterInitialization;
     private final Set<ProcessGroup> startGroupsAfterInitialization;
     private final Set<RemoteGroupPort> 
startRemoteGroupPortsAfterInitialization;
+    private final Set<ConnectorNode> startConnectorsAfterInitialization;
     private final LeaderElectionManager leaderElectionManager;
     private final ClusterCoordinator clusterCoordinator;
     private final FlowEngine validationThreadPool;
@@ -679,6 +680,7 @@ public class FlowController implements 
ReportingTaskProvider, FlowAnalysisRulePr
         startConnectablesAfterInitialization = new HashSet<>();
         startRemoteGroupPortsAfterInitialization = new HashSet<>();
         startGroupsAfterInitialization = new HashSet<>();
+        startConnectorsAfterInitialization = new HashSet<>();
 
         final String gracefulShutdownSecondsVal = 
nifiProperties.getProperty(GRACEFUL_SHUTDOWN_PERIOD);
         long shutdownSecs;
@@ -1221,6 +1223,7 @@ public class FlowController implements 
ReportingTaskProvider, FlowAnalysisRulePr
             startConnectablesAfterInitialization.clear();
             startRemoteGroupPortsAfterInitialization.clear();
             startGroupsAfterInitialization.clear();
+            startConnectorsAfterInitialization.clear();
         } finally {
             writeLock.unlock("purge");
         }
@@ -1490,6 +1493,23 @@ public class FlowController implements 
ReportingTaskProvider, FlowAnalysisRulePr
 
                 LOG.info("Started {} Remote Group Ports transmitting", 
startedTransmitting);
                 startRemoteGroupPortsAfterInitialization.clear();
+
+                LOG.info("Starting {} Connectors", 
startConnectorsAfterInitialization.size());
+                for (final ConnectorNode connectorNode : 
startConnectorsAfterInitialization) {
+                    try {
+                        final ConnectorNode existingConnector = 
connectorRepository.getConnector(connectorNode.getIdentifier());
+                        if (existingConnector == null) {
+                            LOG.debug("Will not start {} because it no longer 
exists", connectorNode);
+                            continue;
+                        }
+
+                        connectorRepository.startConnector(connectorNode);
+                    } catch (final Throwable t) {
+                        LOG.error("Unable to start {}", connectorNode, t);
+                    }
+                }
+
+                startConnectorsAfterInitialization.clear();
             } else {
                 // We don't want to start all of the delayed components. 
However, funnels need to be started anyway
                 // because we don't provide users the ability to start or stop 
them - they are just notional.
@@ -1505,6 +1525,19 @@ public class FlowController implements 
ReportingTaskProvider, FlowAnalysisRulePr
 
                 startConnectablesAfterInitialization.clear();
                 startRemoteGroupPortsAfterInitialization.clear();
+
+                // Explicitly stop Connectors so that their state is properly 
transitioned from UPDATED to STOPPED.
+                for (final ConnectorNode connectorNode : 
startConnectorsAfterInitialization) {
+                    try {
+                        final ConnectorNode existingConnector = 
connectorRepository.getConnector(connectorNode.getIdentifier());
+                        if (existingConnector != null) {
+                            connectorRepository.stopConnector(connectorNode);
+                        }
+                    } catch (final Throwable t) {
+                        LOG.error("Unable to stop {}", connectorNode, t);
+                    }
+                }
+                startConnectorsAfterInitialization.clear();
             }
 
             
flowManager.getRootGroup().findAllRemoteProcessGroups().forEach(RemoteProcessGroup::initialize);
@@ -1574,6 +1607,10 @@ public class FlowController implements 
ReportingTaskProvider, FlowAnalysisRulePr
         return startGroupsAfterInitialization.contains(group);
     }
 
+    public boolean isStartAfterInitialization(final ConnectorNode 
connectorNode) {
+        return startConnectorsAfterInitialization.contains(connectorNode);
+    }
+
     private ContentRepository createContentRepository(final NiFiProperties 
properties) {
         final String implementationClassName = 
properties.getProperty(NiFiProperties.CONTENT_REPOSITORY_IMPLEMENTATION, 
DEFAULT_CONTENT_REPO_IMPLEMENTATION);
         if (implementationClassName == null) {
@@ -2363,6 +2400,19 @@ public class FlowController implements 
ReportingTaskProvider, FlowAnalysisRulePr
         }
     }
 
+    public void startConnector(final ConnectorNode connectorNode) {
+        writeLock.lock();
+        try {
+            if (initialized.get()) {
+                connectorRepository.startConnector(connectorNode);
+            } else {
+                startConnectorsAfterInitialization.add(connectorNode);
+            }
+        } finally {
+            writeLock.unlock("startConnector");
+        }
+    }
+
     public boolean isInitialized() {
         return initialized.get();
     }
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/VersionedDataflowMapper.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/VersionedDataflowMapper.java
index 550cfbe25f8..ccf5391707e 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/VersionedDataflowMapper.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/VersionedDataflowMapper.java
@@ -99,6 +99,10 @@ public class VersionedDataflowMapper {
 
         for (final ConnectorNode connectorNode : 
flowController.getConnectorRepository().getConnectors()) {
             final VersionedConnector versionedConnector = 
flowMapper.mapConnector(connectorNode);
+            if (flowController.isStartAfterInitialization(connectorNode)) {
+                versionedConnector.setScheduledState(ScheduledState.RUNNING);
+            }
+
             connectors.add(versionedConnector);
         }
 
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/VersionedFlowSynchronizer.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/VersionedFlowSynchronizer.java
index 13eb54aec78..3e002d5450e 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/VersionedFlowSynchronizer.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/VersionedFlowSynchronizer.java
@@ -1057,10 +1057,10 @@ public class VersionedFlowSynchronizer implements 
FlowSynchronizer {
                     logger.info("Connector {} of type {} with name {} is not 
in the current flow. Will add Connector.",
                         versionedConnector.getInstanceIdentifier(), 
versionedConnector.getType(), versionedConnector.getName());
 
-                    addConnector(versionedConnector, connectorRepository, 
flowController.getFlowManager());
+                    addConnector(versionedConnector, flowController);
                 } else if (isConnectorConfigurationUpdated(existingConnector, 
versionedConnector)) {
                     logger.info("{} configuration has changed, updating 
configuration", existingConnector);
-                    updateConnector(versionedConnector, connectorRepository);
+                    updateConnector(versionedConnector, flowController);
                 } else {
                     logger.debug("{} configuration is up to date, no update 
necessary", existingConnector);
                 }
@@ -1162,15 +1162,18 @@ public class VersionedFlowSynchronizer implements 
FlowSynchronizer {
         };
     }
 
-    private void addConnector(final VersionedConnector versionedConnector, 
final ConnectorRepository connectorRepository, final FlowManager flowManager) {
+    private void addConnector(final VersionedConnector versionedConnector, 
final FlowController flowController) {
+        final ConnectorRepository connectorRepository = 
flowController.getConnectorRepository();
+        final FlowManager flowManager = flowController.getFlowManager();
         final BundleCoordinate coordinate = 
createBundleCoordinate(extensionManager, versionedConnector.getBundle(), 
versionedConnector.getType());
         final ConnectorNode connectorNode = 
flowManager.createConnector(versionedConnector.getType(), 
versionedConnector.getInstanceIdentifier(), coordinate, false, true);
         connectorRepository.restoreConnector(connectorNode);
-        updateConnector(versionedConnector, connectorRepository);
+        updateConnector(versionedConnector, flowController);
     }
 
 
-    private void updateConnector(final VersionedConnector versionedConnector, 
final ConnectorRepository connectorRepository) {
+    private void updateConnector(final VersionedConnector versionedConnector, 
final FlowController flowController) {
+        final ConnectorRepository connectorRepository = 
flowController.getConnectorRepository();
         final ConnectorNode connectorNode = 
connectorRepository.getConnector(versionedConnector.getInstanceIdentifier());
 
         connectorRepository.updateConnector(connectorNode, 
versionedConnector.getName());
@@ -1182,7 +1185,7 @@ public class VersionedFlowSynchronizer implements 
FlowSynchronizer {
 
             final ScheduledState desiredState = 
versionedConnector.getScheduledState();
             if (desiredState == ScheduledState.RUNNING) {
-                connectorRepository.startConnector(connectorNode);
+                flowController.startConnector(connectorNode);
             } else if (desiredState == ScheduledState.ENABLED) {
                 connectorRepository.stopConnector(connectorNode);
             }
@@ -1514,6 +1517,9 @@ public class VersionedFlowSynchronizer implements 
FlowSynchronizer {
         if (!CollectionUtils.isEmpty(dataflow.getParameterContexts())) {
             return false;
         }
+        if (!CollectionUtils.isEmpty(dataflow.getConnectors())) {
+            return false;
+        }
 
         final VersionedProcessGroup rootGroup = dataflow.getRootGroup();
         return isFlowEmpty(rootGroup);
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/serialization/VersionedFlowSynchronizerTest.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/serialization/VersionedFlowSynchronizerTest.java
index f526e25cb44..71173f8a89f 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/serialization/VersionedFlowSynchronizerTest.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/serialization/VersionedFlowSynchronizerTest.java
@@ -18,6 +18,7 @@ package org.apache.nifi.controller.serialization;
 
 import org.apache.nifi.cluster.protocol.DataFlow;
 import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.connector.ConnectorNode;
 import org.apache.nifi.components.connector.ConnectorRepository;
 import org.apache.nifi.controller.FlowController;
 import org.apache.nifi.controller.ReportingTaskNode;
@@ -30,6 +31,7 @@ import 
org.apache.nifi.controller.service.ControllerServiceProvider;
 import org.apache.nifi.encrypt.PropertyEncryptor;
 import org.apache.nifi.flow.Bundle;
 import org.apache.nifi.flow.ScheduledState;
+import org.apache.nifi.flow.VersionedConnector;
 import org.apache.nifi.flow.VersionedControllerService;
 import org.apache.nifi.flow.VersionedProcessGroup;
 import org.apache.nifi.flow.VersionedReportingTask;
@@ -63,6 +65,7 @@ import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -271,6 +274,10 @@ class VersionedFlowSynchronizerTest {
     }
 
     private void setFlowController() {
+        setFlowController(mock(ConnectorRepository.class));
+    }
+
+    private void setFlowController(final ConnectorRepository 
connectorRepository) {
         when(rootGroup.isEmpty()).thenReturn(false);
         when(flowController.getSnippetManager()).thenReturn(snippetManager);
         when(snippetManager.export()).thenReturn(new byte[]{});
@@ -281,8 +288,62 @@ class VersionedFlowSynchronizerTest {
         
when(flowController.createVersionedComponentStateLookup(any())).thenReturn(stateLookup);
         
when(flowController.getControllerServiceProvider()).thenReturn(controllerServiceProvider);
 
-        final ConnectorRepository connectorRepository = 
mock(ConnectorRepository.class);
         
when(connectorRepository.getConnectors()).thenReturn(Collections.emptyList());
         
when(flowController.getConnectorRepository()).thenReturn(connectorRepository);
     }
+
+    @Test
+    void testSyncInheritConnectorStartRoutedThroughFlowController() {
+        setRootGroup();
+
+        final String connectorId = UUID.randomUUID().toString();
+        final String connectorType = 
"org.apache.nifi.connectors.TestConnector";
+
+        final VersionedConnector versionedConnector = new VersionedConnector();
+        versionedConnector.setInstanceIdentifier(connectorId);
+        versionedConnector.setName("Test Connector");
+        versionedConnector.setType(connectorType);
+        versionedConnector.setBundle(CORE_BUNDLE);
+        versionedConnector.setScheduledState(ScheduledState.RUNNING);
+
+        final ConnectorRepository connectorRepository = 
mock(ConnectorRepository.class);
+        final ConnectorNode connectorNode = mock(ConnectorNode.class);
+        
when(connectorRepository.getConnector(eq(connectorId))).thenReturn(null).thenReturn(connectorNode);
+        when(flowManager.createConnector(eq(connectorType), eq(connectorId), 
any(), eq(false), eq(true))).thenReturn(connectorNode);
+
+        setFlowController(connectorRepository);
+        
when(versionedDataflow.getConnectors()).thenReturn(List.of(versionedConnector));
+
+        versionedFlowSynchronizer.sync(flowController, dataFlow, flowService, 
BundleUpdateStrategy.USE_SPECIFIED_OR_GHOST);
+
+        verify(flowController).startConnector(connectorNode);
+    }
+
+    @Test
+    void testSyncInheritConnectorNotStartedWhenEnabled() {
+        setRootGroup();
+
+        final String connectorId = UUID.randomUUID().toString();
+        final String connectorType = 
"org.apache.nifi.connectors.TestConnector";
+
+        final VersionedConnector versionedConnector = new VersionedConnector();
+        versionedConnector.setInstanceIdentifier(connectorId);
+        versionedConnector.setName("Test Connector");
+        versionedConnector.setType(connectorType);
+        versionedConnector.setBundle(CORE_BUNDLE);
+        versionedConnector.setScheduledState(ScheduledState.ENABLED);
+
+        final ConnectorRepository connectorRepository = 
mock(ConnectorRepository.class);
+        final ConnectorNode connectorNode = mock(ConnectorNode.class);
+        
when(connectorRepository.getConnector(eq(connectorId))).thenReturn(null).thenReturn(connectorNode);
+        when(flowManager.createConnector(eq(connectorType), eq(connectorId), 
any(), eq(false), eq(true))).thenReturn(connectorNode);
+
+        setFlowController(connectorRepository);
+        
when(versionedDataflow.getConnectors()).thenReturn(List.of(versionedConnector));
+
+        versionedFlowSynchronizer.sync(flowController, dataFlow, flowService, 
BundleUpdateStrategy.USE_SPECIFIED_OR_GHOST);
+
+        verify(flowController, never()).startConnector(any());
+        verify(connectorRepository).stopConnector(connectorNode);
+    }
 }
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
new file mode 100644
index 00000000000..2f85b4f7b20
--- /dev/null
+++ 
b/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/connectors/ConnectorAutoResumeIT.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.tests.system.connectors;
+
+import org.apache.nifi.components.connector.ConnectorState;
+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.entity.ConnectorEntity;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * 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}.
+ */
+public class ConnectorAutoResumeIT extends NiFiSystemIT {
+
+    private static final Logger logger = 
LoggerFactory.getLogger(ConnectorAutoResumeIT.class);
+
+    @Override
+    protected boolean isDestroyEnvironmentAfterEachTest() {
+        return true;
+    }
+
+    @Override
+    protected boolean isAllowFactoryReuse() {
+        return false;
+    }
+
+    @Test
+    public void testConnectorNotResumedWhenAutoResumeDisabled() throws 
NiFiClientException, IOException, InterruptedException {
+        final ConnectorEntity connector = 
getClientUtil().createConnector("NopConnector");
+        final String connectorId = connector.getId();
+
+        getClientUtil().applyConnectorUpdate(connector);
+        getClientUtil().waitForValidConnector(connectorId);
+
+        getClientUtil().startConnector(connectorId);
+        getClientUtil().waitForConnectorState(connectorId, 
ConnectorState.RUNNING);
+        logger.info("Connector {} is RUNNING", connectorId);
+
+        getNiFiInstance().stop();
+        logger.info("NiFi stopped");
+
+        getNiFiInstance().setProperty(NiFiProperties.AUTO_RESUME_STATE, 
"false");
+        getNiFiInstance().start();
+        setupClient();
+        logger.info("NiFi restarted with autoResumeState=false");
+
+        final ConnectorEntity connectorAfterRestart = 
getNifiClient().getConnectorClient().getConnector(connectorId);
+        final String stateAfterRestart = 
connectorAfterRestart.getComponent().getState();
+
+        assertEquals(ConnectorState.STOPPED.name(), stateAfterRestart);
+    }
+}

Reply via email to