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

markap14 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 7fbc1b6a518 NIFI-15751 Improve Nifi Cluster Sync with Connectors 
(#11079)
7fbc1b6a518 is described below

commit 7fbc1b6a51815526a1d36f7113716a87300e3eed
Author: Kevin Doran <[email protected]>
AuthorDate: Thu Apr 2 15:00:56 2026 -0400

    NIFI-15751 Improve Nifi Cluster Sync with Connectors (#11079)
    
    * NIFI-15751 Delegate connector sync from VFS to ConnectorRepository
    
    Refactor VersionedFlowSynchronizer to delegate connector synchronization
    entirely to ConnectorRepository, implementing state-aware behavior for all
    ConnectorState values during flow inheritance.
    
    - Introduce ConnectorSyncDirective (ALLOW/REJECT/REMOVE) returned by
      ConnectorConfigurationProvider.verifySyncable, enabling providers to
      override ScheduledState and supply working config + name
    - Introduce ConnectorSyncResult enum for sync outcomes
    - Move connector creation, config comparison, and lifecycle management
      from VFS into StandardConnectorRepository.syncConnector
    - Add configurable sync timeout (nifi.components.connectors.sync.timeout)
      for waiting on transient ConnectorStates (STARTING, STOPPING, PURGING)
    - Simplify VFS inheritConnectors to iterate and delegate
---
 .../java/org/apache/nifi/util/NiFiProperties.java  |   2 +
 .../connector/ConnectorConfigurationProvider.java  |  31 ++
 .../connector/ConnectorSyncDirective.java          | 144 +++++++
 .../components/connector/ConnectorRepository.java  |  23 ++
 .../ConnectorRepositoryInitializationContext.java  |  12 +
 .../components/connector/ConnectorSyncResult.java  | 119 ++++++
 ...StandardConnectorRepoInitializationContext.java |  12 +-
 .../connector/StandardConnectorRepository.java     | 342 ++++++++++++++++-
 .../org/apache/nifi/controller/FlowController.java |   7 +-
 .../serialization/VersionedFlowSynchronizer.java   | 169 ++-------
 .../connector/TestStandardConnectorRepository.java | 413 +++++++++++++++++++++
 .../VersionedFlowSynchronizerTest.java             | 127 ++++++-
 .../system/connectors/ClusteredConnectorIT.java    |  82 ++++
 13 files changed, 1327 insertions(+), 156 deletions(-)

diff --git 
a/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java
 
b/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java
index eef06942f01..5b4648b23e8 100644
--- 
a/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java
+++ 
b/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java
@@ -141,6 +141,8 @@ public class NiFiProperties extends ApplicationProperties {
 
     // Connector Repository properties
     public static final String CONNECTOR_REPOSITORY_IMPLEMENTATION = 
"nifi.components.connectors.repository.implementation";
+    public static final String CONNECTOR_SYNC_TIMEOUT = 
"nifi.components.connectors.sync.timeout";
+    public static final String DEFAULT_CONNECTOR_SYNC_TIMEOUT = "5 mins";
 
     // Connector Configuration Provider properties
     public static final String CONNECTOR_CONFIGURATION_PROVIDER_IMPLEMENTATION 
= "nifi.components.connectors.configuration.provider.implementation";
diff --git 
a/nifi-framework-api/src/main/java/org/apache/nifi/components/connector/ConnectorConfigurationProvider.java
 
b/nifi-framework-api/src/main/java/org/apache/nifi/components/connector/ConnectorConfigurationProvider.java
index 4d2ff3ecc88..a0d5314a2f9 100644
--- 
a/nifi-framework-api/src/main/java/org/apache/nifi/components/connector/ConnectorConfigurationProvider.java
+++ 
b/nifi-framework-api/src/main/java/org/apache/nifi/components/connector/ConnectorConfigurationProvider.java
@@ -17,6 +17,8 @@
 
 package org.apache.nifi.components.connector;
 
+import org.apache.nifi.flow.ScheduledState;
+
 import java.io.InputStream;
 import java.util.Optional;
 
@@ -99,6 +101,35 @@ public interface ConnectorConfigurationProvider {
      */
     void verifyCreate(String connectorId);
 
+    /**
+     * Determines how the connector repository should handle synchronization 
for the given
+     * connector during flow inheritance (cluster join). The provider examines 
the external
+     * state of the connector and returns a {@link ConnectorSyncDirective} 
indicating whether
+     * to allow, reject, or remove the connector.
+     *
+     * <p>When the directive action is {@link 
ConnectorSyncDirective.Action#ALLOW}, the
+     * directive may optionally include:</p>
+     * <ul>
+     *   <li>A {@link ConnectorWorkingConfiguration} with the provider's 
working config and name
+     *       (overriding the potentially stale values from the versioned 
flow)</li>
+     *   <li>A {@link ScheduledState} override (correcting stale run intent 
from the versioned flow)</li>
+     * </ul>
+     *
+     * <p>This method combines the verify and load operations into a single 
call to avoid
+     * redundant round-trips to the external store.</p>
+     *
+     * <p>The default implementation returns {@link 
ConnectorSyncDirective#allow()} with no
+     * overrides, meaning the versioned flow's values are used for everything. 
This is the
+     * behavior for Apache NiFi when no provider is configured.</p>
+     *
+     * @param connectorId the identifier of the connector to check
+     * @param proposedScheduledState the ScheduledState from the versioned flow
+     * @return a directive indicating how to handle this connector during sync
+     */
+    default ConnectorSyncDirective getSyncDirective(final String connectorId, 
final ScheduledState proposedScheduledState) {
+        return ConnectorSyncDirective.allow();
+    }
+
     /**
      * Stores an asset to the local {@link org.apache.nifi.asset.AssetManager} 
and to the external
      * store. The provider records the NiFi UUID to external identifier 
mapping in its local state.
diff --git 
a/nifi-framework-api/src/main/java/org/apache/nifi/components/connector/ConnectorSyncDirective.java
 
b/nifi-framework-api/src/main/java/org/apache/nifi/components/connector/ConnectorSyncDirective.java
new file mode 100644
index 00000000000..3ca70c6ad11
--- /dev/null
+++ 
b/nifi-framework-api/src/main/java/org/apache/nifi/components/connector/ConnectorSyncDirective.java
@@ -0,0 +1,144 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.nifi.components.connector;
+
+import org.apache.nifi.flow.ScheduledState;
+
+/**
+ * Directive returned by {@link 
ConnectorConfigurationProvider#getSyncDirective(String, ScheduledState)}
+ * indicating how the connector repository should handle synchronization for a 
connector during
+ * flow inheritance.
+ */
+public class ConnectorSyncDirective {
+
+    /**
+     * The action the connector repository should take for this connector 
during flow sync.
+     */
+    public enum Action {
+        /**
+         * Proceed with synchronization. The directive may optionally include a
+         * {@link ScheduledState} override and/or a {@link 
ConnectorWorkingConfiguration}
+         * containing the provider's working config and name.
+         */
+        ALLOW,
+
+        /**
+         * Do not synchronize this connector. The connector should be created 
locally
+         * (if not already present) and marked invalid so that a background 
repair
+         * process can attempt synchronization later when conditions improve.
+         */
+        REJECT,
+
+        /**
+         * This connector should not exist on this node. If it exists locally, 
remove
+         * it from the repository. If it does not exist, do not create it. 
This is used
+         * when the external system indicates the connector is being deleted 
or has been deleted.
+         */
+        REMOVE
+    }
+
+    private static final ConnectorSyncDirective ALLOW_DEFAULT = new 
ConnectorSyncDirective(Action.ALLOW, null, null);
+    private static final ConnectorSyncDirective REJECT_DIRECTIVE = new 
ConnectorSyncDirective(Action.REJECT, null, null);
+    private static final ConnectorSyncDirective REMOVE_DIRECTIVE = new 
ConnectorSyncDirective(Action.REMOVE, null, null);
+
+    private final Action action;
+    private final ScheduledState scheduledStateOverride;
+    private final ConnectorWorkingConfiguration workingConfiguration;
+
+    private ConnectorSyncDirective(final Action action, final ScheduledState 
scheduledStateOverride,
+                                   final ConnectorWorkingConfiguration 
workingConfiguration) {
+        this.action = action;
+        this.scheduledStateOverride = scheduledStateOverride;
+        this.workingConfiguration = workingConfiguration;
+    }
+
+    /**
+     * Returns an ALLOW directive with no overrides. The connector repository 
will use the
+     * versioned flow's name, working config, and ScheduledState as-is. This 
is the default
+     * behavior when no {@link ConnectorConfigurationProvider} is configured 
(Apache NiFi).
+     */
+    public static ConnectorSyncDirective allow() {
+        return ALLOW_DEFAULT;
+    }
+
+    /**
+     * Returns an ALLOW directive with the provider's working configuration 
(name + working
+     * config steps) and no ScheduledState override.
+     *
+     * @param workingConfiguration the provider's working configuration 
including name
+     */
+    public static ConnectorSyncDirective allow(final 
ConnectorWorkingConfiguration workingConfiguration) {
+        return new ConnectorSyncDirective(Action.ALLOW, null, 
workingConfiguration);
+    }
+
+    /**
+     * Returns an ALLOW directive with the provider's working configuration 
and a
+     * ScheduledState override. The override replaces the versioned flow's 
ScheduledState,
+     * which may be stale due to in-flight DPS tasks.
+     *
+     * @param workingConfiguration the provider's working configuration 
including name
+     * @param scheduledStateOverride the ScheduledState to use instead of the 
versioned flow's value
+     */
+    public static ConnectorSyncDirective allow(final 
ConnectorWorkingConfiguration workingConfiguration,
+                                               final ScheduledState 
scheduledStateOverride) {
+        return new ConnectorSyncDirective(Action.ALLOW, 
scheduledStateOverride, workingConfiguration);
+    }
+
+    /**
+     * Returns a REJECT directive. The connector will be created locally (if 
not already
+     * present) and marked invalid for background repair.
+     */
+    public static ConnectorSyncDirective reject() {
+        return REJECT_DIRECTIVE;
+    }
+
+    /**
+     * Returns a REMOVE directive. The connector should not exist on this node.
+     */
+    public static ConnectorSyncDirective remove() {
+        return REMOVE_DIRECTIVE;
+    }
+
+    public Action getAction() {
+        return action;
+    }
+
+    /**
+     * Returns the ScheduledState override, or {@code null} if the versioned 
flow's
+     * ScheduledState should be used.
+     */
+    public ScheduledState getScheduledStateOverride() {
+        return scheduledStateOverride;
+    }
+
+    /**
+     * Returns the provider's working configuration (name + working config 
steps),
+     * or {@code null} if the versioned flow's values should be used.
+     */
+    public ConnectorWorkingConfiguration getWorkingConfiguration() {
+        return workingConfiguration;
+    }
+
+    @Override
+    public String toString() {
+        return "ConnectorSyncDirective[action=" + action
+                + (scheduledStateOverride != null ? ", 
scheduledStateOverride=" + scheduledStateOverride : "")
+                + (workingConfiguration != null ? ", hasWorkingConfig=true" : 
"")
+                + "]";
+    }
+}
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/components/connector/ConnectorRepository.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/components/connector/ConnectorRepository.java
index cfa5ae5f9a6..6c0a3ba43ea 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/components/connector/ConnectorRepository.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/components/connector/ConnectorRepository.java
@@ -21,6 +21,7 @@ import org.apache.nifi.asset.Asset;
 import org.apache.nifi.components.connector.secrets.SecretsManager;
 import org.apache.nifi.flow.Bundle;
 import org.apache.nifi.flow.VersionedConfigurationStep;
+import org.apache.nifi.flow.VersionedConnector;
 
 import java.io.IOException;
 import java.io.InputStream;
@@ -59,6 +60,28 @@ public interface ConnectorRepository {
      */
     void restoreConnector(ConnectorNode connector);
 
+    /**
+     * Synchronizes a connector during flow inheritance. This method handles 
the full lifecycle:
+     * looking up or creating the local {@link ConnectorNode}, consulting the
+     * {@link ConnectorConfigurationProvider} for external state and 
configuration, checking
+     * the current {@link ConnectorState}, waiting for transient states to 
resolve, comparing
+     * configuration, applying changes via {@link #inheritConfiguration}, and 
updating the
+     * run state.
+     *
+     * <p>The repository owns connector creation — if the connector does not 
exist locally,
+     * the repository will create it (unless the provider directive indicates 
the connector
+     * should be removed or rejected).</p>
+     *
+     * <p>When a {@link ConnectorConfigurationProvider} is configured, the 
provider's
+     * {@link ConnectorConfigurationProvider#getSyncDirective(String, 
org.apache.nifi.flow.ScheduledState)}
+     * method is called to obtain a {@link ConnectorSyncDirective} that may 
override the
+     * working configuration, name, or ScheduledState from the versioned 
flow.</p>
+     *
+     * @param versionedConnector the proposed connector from the versioned flow
+     * @return a {@link ConnectorSyncResult} indicating the outcome
+     */
+    ConnectorSyncResult syncConnector(VersionedConnector versionedConnector);
+
     /**
      * Removes the given Connector from the Repository
      * @param connectorId the identifier of the Connector to remove
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/components/connector/ConnectorRepositoryInitializationContext.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/components/connector/ConnectorRepositoryInitializationContext.java
index 91bca1fcc95..7e6e1db372b 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/components/connector/ConnectorRepositoryInitializationContext.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/components/connector/ConnectorRepositoryInitializationContext.java
@@ -23,6 +23,8 @@ import org.apache.nifi.controller.NodeTypeProvider;
 import org.apache.nifi.controller.flow.FlowManager;
 import org.apache.nifi.nar.ExtensionManager;
 
+import java.time.Duration;
+
 public interface ConnectorRepositoryInitializationContext {
 
     FlowManager getFlowManager();
@@ -47,4 +49,14 @@ public interface ConnectorRepositoryInitializationContext {
         return null;
     }
 
+    /**
+     * Returns the maximum time to wait for a connector in a transient state 
(STARTING, STOPPING, PURGING)
+     * to reach a stable state during flow synchronization.
+     *
+     * @return the sync timeout duration, defaults to 5 minutes
+     */
+    default Duration getConnectorSyncTimeout() {
+        return Duration.ofMinutes(5);
+    }
+
 }
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/components/connector/ConnectorSyncResult.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/components/connector/ConnectorSyncResult.java
new file mode 100644
index 00000000000..03a9a3f7413
--- /dev/null
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/components/connector/ConnectorSyncResult.java
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.nifi.components.connector;
+
+import org.apache.nifi.flow.ScheduledState;
+
+/**
+ * Result of a connector synchronization attempt during flow inheritance.
+ * Contains the outcome, the connector node (if applicable), and the effective
+ * ScheduledState that the caller should apply.
+ */
+public final class ConnectorSyncResult {
+
+    /**
+     * The outcome of the sync attempt.
+     */
+    public enum Outcome {
+        SYNCED,
+        SYNCED_CONFIG_UNCHANGED,
+        REJECTED,
+        FAILED,
+        REMOVED
+    }
+
+    private final Outcome outcome;
+    private final ConnectorNode connectorNode;
+    private final ScheduledState effectiveScheduledState;
+
+    private ConnectorSyncResult(final Outcome outcome, final ConnectorNode 
connectorNode, final ScheduledState effectiveScheduledState) {
+        this.outcome = outcome;
+        this.connectorNode = connectorNode;
+        this.effectiveScheduledState = effectiveScheduledState;
+    }
+
+    /**
+     * Configuration was applied and the connector's run state should be 
updated
+     * to match the effective ScheduledState.
+     */
+    public static ConnectorSyncResult synced(final ConnectorNode 
connectorNode, final ScheduledState effectiveScheduledState) {
+        return new ConnectorSyncResult(Outcome.SYNCED, connectorNode, 
effectiveScheduledState);
+    }
+
+    /**
+     * Connector configuration was unchanged relative to the proposed 
versioned configuration;
+     * no configuration update was applied. The connector's run state should 
still be updated
+     * if it differs from the effective ScheduledState.
+     */
+    public static ConnectorSyncResult syncedConfigUnchanged(final 
ConnectorNode connectorNode, final ScheduledState effectiveScheduledState) {
+        return new ConnectorSyncResult(Outcome.SYNCED_CONFIG_UNCHANGED, 
connectorNode, effectiveScheduledState);
+    }
+
+    /**
+     * The connector was in a state that prevents synchronization. The 
connector
+     * has been marked invalid.
+     */
+    public static ConnectorSyncResult rejected(final ConnectorNode 
connectorNode) {
+        return new ConnectorSyncResult(Outcome.REJECTED, connectorNode, null);
+    }
+
+    /**
+     * Synchronization was attempted but failed. The connector has been marked 
invalid.
+     */
+    public static ConnectorSyncResult failed(final ConnectorNode 
connectorNode) {
+        return new ConnectorSyncResult(Outcome.FAILED, connectorNode, null);
+    }
+
+    /**
+     * The connector was removed from this node because the external provider
+     * indicated it should not exist.
+     */
+    public static ConnectorSyncResult removed() {
+        return new ConnectorSyncResult(Outcome.REMOVED, null, null);
+    }
+
+    public Outcome getOutcome() {
+        return outcome;
+    }
+
+    /**
+     * Returns the connector node, or {@code null} if the connector was 
removed.
+     */
+    public ConnectorNode getConnectorNode() {
+        return connectorNode;
+    }
+
+    /**
+     * Returns the effective ScheduledState that should be applied to the 
connector,
+     * or {@code null} if the connector was rejected, failed, or removed.
+     * The caller is responsible for applying this state using the appropriate
+     * mechanism (e.g., {@code FlowController.startConnector()} which respects
+     * deferred-start semantics).
+     */
+    public ScheduledState getEffectiveScheduledState() {
+        return effectiveScheduledState;
+    }
+
+    @Override
+    public String toString() {
+        return "ConnectorSyncResult[outcome=" + outcome
+                + (connectorNode != null ? ", connector=" + 
connectorNode.getIdentifier() : "")
+                + (effectiveScheduledState != null ? ", effectiveState=" + 
effectiveScheduledState : "")
+                + "]";
+    }
+}
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/StandardConnectorRepoInitializationContext.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/StandardConnectorRepoInitializationContext.java
index a0661fa58f9..10fe9b49af0 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/StandardConnectorRepoInitializationContext.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/StandardConnectorRepoInitializationContext.java
@@ -23,6 +23,8 @@ import org.apache.nifi.controller.NodeTypeProvider;
 import org.apache.nifi.controller.flow.FlowManager;
 import org.apache.nifi.nar.ExtensionManager;
 
+import java.time.Duration;
+
 public class StandardConnectorRepoInitializationContext implements 
ConnectorRepositoryInitializationContext {
     private final FlowManager flowManager;
     private final ExtensionManager extensionManager;
@@ -31,6 +33,7 @@ public class StandardConnectorRepoInitializationContext 
implements ConnectorRepo
     private final NodeTypeProvider nodeTypeProvider;
     private final ConnectorRequestReplicator requestReplicator;
     private final ConnectorConfigurationProvider 
connectorConfigurationProvider;
+    private final Duration connectorSyncTimeout;
 
     public StandardConnectorRepoInitializationContext(final FlowManager 
flowManager,
                                                      final ExtensionManager 
extensionManager,
@@ -38,7 +41,8 @@ public class StandardConnectorRepoInitializationContext 
implements ConnectorRepo
                                                      final AssetManager 
assetManager,
                                                      final NodeTypeProvider 
nodeTypeProvider,
                                                      final 
ConnectorRequestReplicator requestReplicator,
-                                                     final 
ConnectorConfigurationProvider connectorConfigurationProvider) {
+                                                     final 
ConnectorConfigurationProvider connectorConfigurationProvider,
+                                                     final Duration 
connectorSyncTimeout) {
         this.flowManager = flowManager;
         this.extensionManager = extensionManager;
         this.secretsManager = secretsManager;
@@ -46,6 +50,7 @@ public class StandardConnectorRepoInitializationContext 
implements ConnectorRepo
         this.nodeTypeProvider = nodeTypeProvider;
         this.requestReplicator = requestReplicator;
         this.connectorConfigurationProvider = connectorConfigurationProvider;
+        this.connectorSyncTimeout = connectorSyncTimeout;
     }
 
     @Override
@@ -82,4 +87,9 @@ public class StandardConnectorRepoInitializationContext 
implements ConnectorRepo
     public ConnectorConfigurationProvider getConnectorConfigurationProvider() {
         return connectorConfigurationProvider;
     }
+
+    @Override
+    public Duration getConnectorSyncTimeout() {
+        return connectorSyncTimeout;
+    }
 }
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/StandardConnectorRepository.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/StandardConnectorRepository.java
index 2df018c6382..0b0382ccfc2 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/StandardConnectorRepository.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/StandardConnectorRepository.java
@@ -20,14 +20,20 @@ package org.apache.nifi.components.connector;
 import org.apache.nifi.annotation.lifecycle.OnRemoved;
 import org.apache.nifi.asset.Asset;
 import org.apache.nifi.asset.AssetManager;
+import org.apache.nifi.bundle.BundleCoordinate;
 import org.apache.nifi.components.connector.secrets.SecretsManager;
+import org.apache.nifi.controller.flow.FlowManager;
 import org.apache.nifi.engine.FlowEngine;
 import org.apache.nifi.flow.Bundle;
+import org.apache.nifi.flow.ScheduledState;
 import org.apache.nifi.flow.VersionedConfigurationStep;
+import org.apache.nifi.flow.VersionedConnector;
 import org.apache.nifi.flow.VersionedConnectorValueReference;
 import org.apache.nifi.nar.ExtensionManager;
 import org.apache.nifi.nar.NarCloseable;
+import org.apache.nifi.util.BundleUtils;
 import org.apache.nifi.util.ReflectionUtils;
+import org.apache.nifi.web.api.dto.BundleDTO;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -39,34 +45,43 @@ import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Future;
+import java.util.function.Function;
+import java.util.stream.Collectors;
 
 public class StandardConnectorRepository implements ConnectorRepository {
     private static final Logger logger = 
LoggerFactory.getLogger(StandardConnectorRepository.class);
+    private static final Duration SYNC_POLL_INTERVAL = Duration.ofSeconds(2);
 
     private final Map<String, ConnectorNode> connectors = new 
ConcurrentHashMap<>();
     private final FlowEngine lifecycleExecutor = new FlowEngine(8, "NiFi 
Connector Lifecycle");
 
+    private volatile FlowManager flowManager;
     private volatile ExtensionManager extensionManager;
     private volatile ConnectorRequestReplicator requestReplicator;
     private volatile SecretsManager secretsManager;
     private volatile AssetManager assetManager;
     private volatile ConnectorConfigurationProvider configurationProvider;
+    private volatile Duration syncTimeout;
 
     @Override
     public void initialize(final ConnectorRepositoryInitializationContext 
context) {
         logger.debug("Initializing ConnectorRepository");
+        this.flowManager = context.getFlowManager();
         this.extensionManager = context.getExtensionManager();
         this.requestReplicator = context.getRequestReplicator();
         this.secretsManager = context.getSecretsManager();
         this.assetManager = context.getAssetManager();
         this.configurationProvider = 
context.getConnectorConfigurationProvider();
-        logger.debug("Successfully initialized ConnectorRepository with 
configurationProvider={}", configurationProvider != null ? 
configurationProvider.getClass().getSimpleName() : "null");
+        this.syncTimeout = context.getConnectorSyncTimeout();
+        logger.debug("Successfully initialized ConnectorRepository with 
configurationProvider={}, syncTimeout={}",
+                configurationProvider != null ? 
configurationProvider.getClass().getSimpleName() : "null", syncTimeout);
     }
 
     @Override
@@ -91,16 +106,298 @@ public class StandardConnectorRepository implements 
ConnectorRepository {
         logger.debug("Successfully restored {}", connector);
     }
 
+    @Override
+    public ConnectorSyncResult syncConnector(final VersionedConnector 
versionedConnector) {
+        final String connectorId = versionedConnector.getInstanceIdentifier();
+        final ScheduledState proposedScheduledState = 
versionedConnector.getScheduledState();
+        logger.debug("syncConnector called for connector [{}]", connectorId);
+
+        // Consult the provider for external state checks and working config
+        final ConnectorSyncDirective directive;
+        if (configurationProvider != null) {
+            try {
+                directive = 
configurationProvider.getSyncDirective(connectorId, proposedScheduledState);
+            } catch (final Exception e) {
+                logger.error("Configuration provider threw exception during 
getSyncDirective for connector [{}]: {}", connectorId, e.getMessage(), e);
+                final ConnectorNode existingNode = 
ensureConnectorNodeExists(versionedConnector);
+                existingNode.markInvalid("Flow Synchronization Failure",
+                        "Configuration provider error during synchronization: 
" + e.getMessage());
+                return ConnectorSyncResult.failed(existingNode);
+            }
+        } else {
+            directive = ConnectorSyncDirective.allow();
+        }
+
+        logger.debug("Connector [{}] sync directive: {}", connectorId, 
directive);
+
+        // Handle REMOVE: connector should not exist on this node
+        if (directive.getAction() == ConnectorSyncDirective.Action.REMOVE) {
+            final ConnectorNode existingNode = connectors.get(connectorId);
+            if (existingNode != null) {
+                logger.info("Removing connector [{}] (state={}) from local 
repository per provider REMOVE directive",
+                        connectorId, existingNode.getCurrentState());
+                stopConnectorAndAwait(existingNode);
+                purgeConnectorAndAwait(existingNode);
+                logger.debug("Connector [{}] stopped and purged (state={}); 
calling removeConnector", connectorId, existingNode.getCurrentState());
+                removeConnector(connectorId);
+                logger.info("Successfully removed connector [{}] per REMOVE 
directive", connectorId);
+            } else {
+                logger.debug("Connector [{}] not present locally; REMOVE 
directive is a no-op", connectorId);
+            }
+            return ConnectorSyncResult.removed();
+        }
+
+        // Handle REJECT: create if needed (for background repair), mark 
invalid
+        if (directive.getAction() == ConnectorSyncDirective.Action.REJECT) {
+            final ConnectorNode node = 
ensureConnectorNodeExists(versionedConnector);
+            logger.warn("Connector [{}] rejected by provider during sync; 
marking invalid", connectorId);
+            node.markInvalid("Flow Synchronization Failure",
+                    "Configuration provider rejected synchronization for this 
connector");
+            return ConnectorSyncResult.rejected(node);
+        }
+
+        // ALLOW: proceed with sync
+        // Look up or create the connector node
+        ConnectorNode connector = connectors.get(connectorId);
+        final boolean isNewConnector = connector == null;
+        if (isNewConnector) {
+            connector = createConnectorNode(versionedConnector);
+            connectors.put(connectorId, connector);
+            logger.info("Created new connector node [{}] of type [{}]", 
connectorId, versionedConnector.getType());
+        }
+
+        // Check local ConnectorState and handle per the agreed behavior matrix
+        ConnectorState currentState = connector.getCurrentState();
+
+        if (isWaitableState(currentState)) {
+            logger.info("{} is in transient state {}; waiting for stable state 
(timeout={})", connector, currentState, syncTimeout);
+            currentState = waitForStableState(connector, currentState);
+            if (isWaitableState(currentState)) {
+                logger.error("{} timed out waiting for transient state {} to 
resolve", connector, currentState);
+                connector.markInvalid("Flow Synchronization Failure",
+                        "Timed out waiting for connector to leave transient 
state " + currentState);
+                return ConnectorSyncResult.rejected(connector);
+            }
+        }
+
+        if (isRejectableState(currentState)) {
+            logger.warn("{} is in state {} which cannot be synchronized; 
marking invalid", connector, currentState);
+            connector.markInvalid("Flow Synchronization Failure",
+                    "Connector cannot be synchronized while in state " + 
currentState);
+            return ConnectorSyncResult.rejected(connector);
+        }
+
+        // Determine effective name, working config, and ScheduledState
+        final ConnectorWorkingConfiguration providerConfig = 
directive.getWorkingConfiguration();
+
+        final String effectiveName = (providerConfig != null && 
providerConfig.getName() != null)
+                ? providerConfig.getName()
+                : versionedConnector.getName();
+
+        final List<VersionedConfigurationStep> effectiveWorkingConfig = 
(providerConfig != null && providerConfig.getWorkingFlowConfiguration() != null)
+                ? providerConfig.getWorkingFlowConfiguration()
+                : versionedConnector.getWorkingFlowConfiguration();
+
+        final List<VersionedConfigurationStep> effectiveActiveConfig = 
versionedConnector.getActiveFlowConfiguration();
+
+        final ScheduledState effectiveScheduledState = 
(directive.getScheduledStateOverride() != null)
+                ? directive.getScheduledStateOverride()
+                : proposedScheduledState;
+
+        // Set name locally (no provider.save())
+        connector.setName(effectiveName);
+
+        // Compare config and sync if changed
+        final boolean wasRunning = currentState == ConnectorState.RUNNING;
+        final boolean configChanged = isNewConnector || 
isConfigurationUpdated(connector, effectiveActiveConfig, 
effectiveWorkingConfig);
+
+        if (configChanged) {
+            logger.info("{} configuration needs synchronization", connector);
+            try {
+                inheritConfiguration(connector, effectiveActiveConfig, 
effectiveWorkingConfig, versionedConnector.getBundle());
+            } catch (final Exception e) {
+                logger.error("{} failed to inherit configuration", connector, 
e);
+                if (wasRunning) {
+                    try {
+                        stopConnector(connector);
+                    } catch (final Exception stopEx) {
+                        logger.error("{} also failed to stop after sync 
failure", connector, stopEx);
+                    }
+                }
+                return ConnectorSyncResult.failed(connector);
+            }
+        } else {
+            logger.debug("{} configuration is up to date, no update 
necessary", connector);
+        }
+
+        return configChanged
+                ? ConnectorSyncResult.synced(connector, 
effectiveScheduledState)
+                : ConnectorSyncResult.syncedConfigUnchanged(connector, 
effectiveScheduledState);
+    }
+
+    private ConnectorNode ensureConnectorNodeExists(final VersionedConnector 
versionedConnector) {
+        final String connectorId = versionedConnector.getInstanceIdentifier();
+        ConnectorNode node = connectors.get(connectorId);
+        if (node == null) {
+            node = createConnectorNode(versionedConnector);
+            connectors.put(connectorId, node);
+            logger.info("Created connector node [{}] for reject/error 
handling", connectorId);
+        }
+        return node;
+    }
+
+    private ConnectorNode createConnectorNode(final VersionedConnector 
versionedConnector) {
+        final BundleCoordinate coordinate = 
resolveBundleCoordinate(versionedConnector.getBundle(), 
versionedConnector.getType());
+        return flowManager.createConnector(versionedConnector.getType(), 
versionedConnector.getInstanceIdentifier(), coordinate, false, true);
+    }
+
+    private BundleCoordinate resolveBundleCoordinate(final Bundle bundle, 
final String componentType) {
+        try {
+            final BundleDTO bundleDto = new BundleDTO(bundle.getGroup(), 
bundle.getArtifact(), bundle.getVersion());
+            return BundleUtils.getCompatibleBundle(extensionManager, 
componentType, bundleDto);
+        } catch (final IllegalStateException e) {
+            return new BundleCoordinate(bundle.getGroup(), 
bundle.getArtifact(), bundle.getVersion());
+        }
+    }
+
+    private boolean isWaitableState(final ConnectorState state) {
+        return state == ConnectorState.STARTING
+                || state == ConnectorState.STOPPING
+                || state == ConnectorState.PURGING;
+    }
+
+    private boolean isRejectableState(final ConnectorState state) {
+        return state == ConnectorState.DRAINING
+                || state == ConnectorState.PREPARING_FOR_UPDATE
+                || state == ConnectorState.UPDATING;
+    }
+
+    private ConnectorState waitForStableState(final ConnectorNode connector, 
final ConnectorState initialState) {
+        final long deadlineNanos = System.nanoTime() + syncTimeout.toNanos();
+        int iterations = 0;
+        ConnectorState current = initialState;
+
+        while (isWaitableState(current) && System.nanoTime() < deadlineNanos) {
+            try {
+                Thread.sleep(SYNC_POLL_INTERVAL.toMillis());
+            } catch (final InterruptedException e) {
+                Thread.currentThread().interrupt();
+                logger.warn("{} interrupted while waiting for stable state", 
connector);
+                return current;
+            }
+
+            current = connector.getCurrentState();
+            iterations++;
+            if (iterations % 10 == 0) {
+                final long elapsedSeconds = Duration.ofNanos(System.nanoTime() 
- (deadlineNanos - syncTimeout.toNanos())).toSeconds();
+                logger.info("Still waiting for {} to leave state {}; current 
state is {}; elapsed={}s",
+                        connector, initialState, current, elapsedSeconds);
+            }
+        }
+
+        return current;
+    }
+
+    // --- Configuration comparison ---
+
+    private boolean isConfigurationUpdated(final ConnectorNode 
existingConnector,
+                                           final 
List<VersionedConfigurationStep> effectiveActiveConfig,
+                                           final 
List<VersionedConfigurationStep> effectiveWorkingConfig) {
+        final ConnectorConfiguration activeConfig = 
existingConnector.getActiveFlowContext().getConfigurationContext().toConnectorConfiguration();
+        final boolean activeContextChanged = 
isConfigurationUpdated(activeConfig, effectiveActiveConfig);
+
+        final ConnectorConfiguration workingConfig = 
existingConnector.getWorkingFlowContext().getConfigurationContext().toConnectorConfiguration();
+        final boolean workingContextChanged = 
isConfigurationUpdated(workingConfig, effectiveWorkingConfig);
+
+        return activeContextChanged || workingContextChanged;
+    }
+
+    private boolean isConfigurationUpdated(final ConnectorConfiguration 
existingConfiguration, final List<VersionedConfigurationStep> 
versionedConfigurationSteps) {
+        if (versionedConfigurationSteps == null || 
versionedConfigurationSteps.isEmpty()) {
+            return existingConfiguration != null && 
!existingConfiguration.getNamedStepConfigurations().isEmpty();
+        }
+
+        final Set<NamedStepConfiguration> existingStepConfigurations = 
existingConfiguration.getNamedStepConfigurations();
+        if (existingStepConfigurations.size() != 
versionedConfigurationSteps.size()) {
+            return true;
+        }
+
+        final Map<String, NamedStepConfiguration> existingStepsByName = 
existingStepConfigurations.stream()
+                .collect(Collectors.toMap(NamedStepConfiguration::stepName, 
Function.identity()));
+
+        for (final VersionedConfigurationStep versionedStep : 
versionedConfigurationSteps) {
+            final NamedStepConfiguration existingStep = 
existingStepsByName.get(versionedStep.getName());
+            if (existingStep == null) {
+                return true;
+            }
+
+            if (isConfigurationStepUpdated(existingStep, versionedStep)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    private boolean isConfigurationStepUpdated(final NamedStepConfiguration 
existingStep, final VersionedConfigurationStep versionedStep) {
+        final Map<String, ConnectorValueReference> existingProperties = 
existingStep.configuration().getPropertyValues();
+
+        final Map<String, VersionedConnectorValueReference> 
versionedProperties = versionedStep.getProperties();
+        if (versionedProperties == null || versionedProperties.isEmpty()) {
+            return existingProperties != null && !existingProperties.isEmpty();
+        }
+
+        if (existingProperties == null || existingProperties.size() != 
versionedProperties.size()) {
+            return true;
+        }
+
+        for (final Map.Entry<String, VersionedConnectorValueReference> 
versionedEntry : versionedProperties.entrySet()) {
+            final String propertyName = versionedEntry.getKey();
+            final VersionedConnectorValueReference versionedRef = 
versionedEntry.getValue();
+            final ConnectorValueReference existingRef = 
existingProperties.get(propertyName);
+
+            if (!valueReferencesEqual(versionedRef, existingRef)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    private boolean valueReferencesEqual(final 
VersionedConnectorValueReference versionedReference, final 
ConnectorValueReference existingReference) {
+        if (versionedReference == null && existingReference == null) {
+            return true;
+        }
+        if (versionedReference == null || existingReference == null) {
+            return false;
+        }
+
+        final String versionedValueType = versionedReference.getValueType();
+        final String existingValueType = 
existingReference.getValueType().name();
+        if (!Objects.equals(versionedValueType, existingValueType)) {
+            return false;
+        }
+
+        return switch (existingReference) {
+            case StringLiteralValue stringLiteral -> 
Objects.equals(stringLiteral.getValue(), versionedReference.getValue());
+            case AssetReference assetRef -> 
Objects.equals(assetRef.getAssetIdentifiers(), 
versionedReference.getAssetIds());
+            case SecretReference secretRef -> 
Objects.equals(secretRef.getProviderId(), versionedReference.getProviderId())
+                    && Objects.equals(secretRef.getSecretName(), 
versionedReference.getSecretName());
+        };
+    }
+
     @Override
     public void removeConnector(final String connectorId) {
-        logger.debug("Removing {}", connectorId);
+        logger.debug("Removing connector [{}]", connectorId);
         final ConnectorNode connectorNode = connectors.get(connectorId);
         if (connectorNode == null) {
             throw new IllegalStateException("No connector found with ID " + 
connectorId);
         }
 
+        logger.debug("Verifying connector [{}] (state={}) can be deleted", 
connectorId, connectorNode.getCurrentState());
         connectorNode.verifyCanDelete();
         if (configurationProvider != null) {
+            logger.debug("Notifying configuration provider of connector [{}] 
deletion", connectorId);
             configurationProvider.delete(connectorId);
         }
         connectors.remove(connectorId);
@@ -111,6 +408,47 @@ public class StandardConnectorRepository implements 
ConnectorRepository {
         }
 
         extensionManager.removeInstanceClassLoader(connectorId);
+        logger.info("Connector [{}] removed from repository", connectorId);
+    }
+
+    private void stopConnectorAndAwait(final ConnectorNode connector) {
+        final String connectorId = connector.getIdentifier();
+        final ConnectorState currentState = connector.getCurrentState();
+        if (currentState == ConnectorState.STOPPED || currentState == 
ConnectorState.UPDATE_FAILED || currentState == ConnectorState.UPDATED) {
+            logger.debug("Connector [{}] is already in state {}; no stop 
needed", connectorId, currentState);
+            return;
+        }
+
+        logger.info("Stopping connector [{}] (current state: {}) and awaiting 
completion", connectorId, currentState);
+        try {
+            final Future<Void> stopFuture = stopConnector(connector);
+            stopFuture.get(syncTimeout.toMillis(), 
java.util.concurrent.TimeUnit.MILLISECONDS);
+            logger.debug("Connector [{}] stopped successfully", connectorId);
+        } catch (final java.util.concurrent.TimeoutException e) {
+            logger.warn("Timed out waiting for connector [{}] to stop", 
connectorId);
+        } catch (final InterruptedException e) {
+            Thread.currentThread().interrupt();
+            logger.warn("Interrupted while waiting for connector [{}] to 
stop", connectorId);
+        } catch (final Exception e) {
+            logger.warn("Failed to stop connector [{}]: {}", connectorId, 
e.getMessage(), e);
+        }
+    }
+
+    private void purgeConnectorAndAwait(final ConnectorNode connector) {
+        final String connectorId = connector.getIdentifier();
+        try {
+            logger.debug("Purging FlowFiles for connector [{}] before 
removal", connectorId);
+            final Future<Void> purgeFuture = connector.purgeFlowFiles("Flow 
Synchronization");
+            purgeFuture.get(syncTimeout.toMillis(), 
java.util.concurrent.TimeUnit.MILLISECONDS);
+            logger.debug("Connector [{}] purged successfully", connectorId);
+        } catch (final java.util.concurrent.TimeoutException e) {
+            logger.warn("Timed out waiting for connector [{}] to purge 
FlowFiles", connectorId);
+        } catch (final InterruptedException e) {
+            Thread.currentThread().interrupt();
+            logger.warn("Interrupted while waiting for connector [{}] to purge 
FlowFiles", connectorId);
+        } catch (final Exception e) {
+            logger.warn("Failed to purge FlowFiles for connector [{}]: {}", 
connectorId, e.getMessage(), e);
+        }
     }
 
     @Override
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 c3de110791e..26137a6ae53 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
@@ -934,6 +934,10 @@ public class FlowController implements 
ReportingTaskProvider, FlowAnalysisRulePr
             
extensionManager.discoverExtensions(extensionManager.getAllBundles(), 
additionalExtensionTypes, false);
             final ConnectorRepository created = 
NarThreadContextClassLoader.createInstance(extensionManager, 
implementationClassName, ConnectorRepository.class, properties);
 
+            final String syncTimeoutValue = 
properties.getProperty(NiFiProperties.CONNECTOR_SYNC_TIMEOUT, 
NiFiProperties.DEFAULT_CONNECTOR_SYNC_TIMEOUT);
+            final long syncTimeoutMillis = (long) 
FormatUtils.getTimeDuration(syncTimeoutValue, TimeUnit.MILLISECONDS);
+            final Duration connectorSyncTimeout = 
Duration.ofMillis(syncTimeoutMillis);
+
             final ConnectorRepositoryInitializationContext 
initializationContext = new StandardConnectorRepoInitializationContext(
                 flowManager,
                 extensionManager,
@@ -941,7 +945,8 @@ public class FlowController implements 
ReportingTaskProvider, FlowAnalysisRulePr
                 assetManager,
                 nodeTypeProvider,
                 requestReplicator,
-                connectorConfigurationProvider
+                connectorConfigurationProvider,
+                connectorSyncTimeout
             );
 
             synchronized (created) {
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 3e002d5450e..2a07ac621fe 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
@@ -26,15 +26,9 @@ import org.apache.nifi.authorization.ManagedAuthorizer;
 import org.apache.nifi.bundle.BundleCoordinate;
 import org.apache.nifi.cluster.protocol.DataFlow;
 import org.apache.nifi.cluster.protocol.StandardDataFlow;
-import org.apache.nifi.components.connector.AssetReference;
-import org.apache.nifi.components.connector.ConnectorConfiguration;
 import org.apache.nifi.components.connector.ConnectorNode;
 import org.apache.nifi.components.connector.ConnectorRepository;
-import org.apache.nifi.components.connector.ConnectorValueReference;
-import org.apache.nifi.components.connector.FlowUpdateException;
-import org.apache.nifi.components.connector.NamedStepConfiguration;
-import org.apache.nifi.components.connector.SecretReference;
-import org.apache.nifi.components.connector.StringLiteralValue;
+import org.apache.nifi.components.connector.ConnectorSyncResult;
 import org.apache.nifi.components.validation.ValidationStatus;
 import org.apache.nifi.connectable.Connectable;
 import org.apache.nifi.connectable.Position;
@@ -64,9 +58,7 @@ import org.apache.nifi.flow.ExecutionEngine;
 import org.apache.nifi.flow.ScheduledState;
 import org.apache.nifi.flow.VersionedAsset;
 import org.apache.nifi.flow.VersionedComponent;
-import org.apache.nifi.flow.VersionedConfigurationStep;
 import org.apache.nifi.flow.VersionedConnector;
-import org.apache.nifi.flow.VersionedConnectorValueReference;
 import org.apache.nifi.flow.VersionedControllerService;
 import org.apache.nifi.flow.VersionedExternalFlow;
 import org.apache.nifi.flow.VersionedFlowAnalysisRule;
@@ -1042,9 +1034,6 @@ public class VersionedFlowSynchronizer implements 
FlowSynchronizer {
     }
 
     private void inheritConnectors(final FlowController flowController, final 
VersionedDataflow dataflow) {
-        // TODO: We need to delete any Connectors that are no longer part of 
the flow.
-        //       This means we need to drain the Connector first, then stop 
it, then delete it. If unable to drain, we must fail...
-        //              perhaps we need a DRAINING state? Or do we just delete 
it and drop the data?
         final ConnectorRepository connectorRepository = 
flowController.getConnectorRepository();
 
         final Set<String> proposedConnectorIds = new HashSet<>();
@@ -1052,145 +1041,39 @@ public class VersionedFlowSynchronizer implements 
FlowSynchronizer {
             for (final VersionedConnector versionedConnector : 
dataflow.getConnectors()) {
                 
proposedConnectorIds.add(versionedConnector.getInstanceIdentifier());
 
-                final ConnectorNode existingConnector = 
connectorRepository.getConnector(versionedConnector.getInstanceIdentifier());
-                if (existingConnector == null) {
-                    logger.info("Connector {} of type {} with name {} is not 
in the current flow. Will add Connector.",
-                        versionedConnector.getInstanceIdentifier(), 
versionedConnector.getType(), versionedConnector.getName());
+                final ConnectorSyncResult result = 
connectorRepository.syncConnector(versionedConnector);
+                logger.info("Connector [{}] sync result: {}", 
versionedConnector.getInstanceIdentifier(), result);
 
-                    addConnector(versionedConnector, flowController);
-                } else if (isConnectorConfigurationUpdated(existingConnector, 
versionedConnector)) {
-                    logger.info("{} configuration has changed, updating 
configuration", existingConnector);
-                    updateConnector(versionedConnector, flowController);
-                } else {
-                    logger.debug("{} configuration is up to date, no update 
necessary", existingConnector);
+                if (result.getEffectiveScheduledState() != null && 
result.getConnectorNode() != null) {
+                    if (result.getEffectiveScheduledState() == 
ScheduledState.RUNNING) {
+                        
flowController.startConnector(result.getConnectorNode());
+                    } else if (result.getEffectiveScheduledState() == 
ScheduledState.ENABLED) {
+                        
connectorRepository.stopConnector(result.getConnectorNode());
+                    }
                 }
             }
         }
 
         for (final ConnectorNode existingConnector : 
connectorRepository.getConnectors()) {
             if 
(!proposedConnectorIds.contains(existingConnector.getIdentifier())) {
-                logger.info("Connector {} is no longer part of the proposed 
flow. Will remove Connector.", existingConnector);
-                connectorRepository.stopConnector(existingConnector);
-                
connectorRepository.removeConnector(existingConnector.getIdentifier());
-            }
-        }
-    }
-
-    private boolean isConnectorConfigurationUpdated(final ConnectorNode 
existingConnector, final VersionedConnector versionedConnector) {
-        final ConnectorConfiguration activeConfig = 
existingConnector.getActiveFlowContext().getConfigurationContext().toConnectorConfiguration();
-        final List<VersionedConfigurationStep> versionedActiveConfig = 
versionedConnector.getActiveFlowConfiguration();
-        final boolean activeContextChanged = 
isConnectorConfigurationUpdated(activeConfig, versionedActiveConfig);
-
-        final ConnectorConfiguration workingConfig = 
existingConnector.getWorkingFlowContext().getConfigurationContext().toConnectorConfiguration();
-        final List<VersionedConfigurationStep> versionedWorkingConfig = 
versionedConnector.getWorkingFlowConfiguration();
-        final boolean workingContextChanged = 
isConnectorConfigurationUpdated(workingConfig, versionedWorkingConfig);
-
-        return activeContextChanged || workingContextChanged;
-    }
-
-    private boolean isConnectorConfigurationUpdated(final 
ConnectorConfiguration existingConfiguration, final 
List<VersionedConfigurationStep> versionedConfigurationSteps) {
-        if (versionedConfigurationSteps == null || 
versionedConfigurationSteps.isEmpty()) {
-            return existingConfiguration != null && 
!existingConfiguration.getNamedStepConfigurations().isEmpty();
-        }
-
-        final Set<NamedStepConfiguration> existingStepConfigurations = 
existingConfiguration.getNamedStepConfigurations();
-        if (existingStepConfigurations.size() != 
versionedConfigurationSteps.size()) {
-            return true;
-        }
-
-        final Map<String, NamedStepConfiguration> existingStepsByName = 
existingStepConfigurations.stream()
-            .collect(Collectors.toMap(NamedStepConfiguration::stepName, 
Function.identity()));
-
-        for (final VersionedConfigurationStep versionedStep : 
versionedConfigurationSteps) {
-            final NamedStepConfiguration existingStep = 
existingStepsByName.get(versionedStep.getName());
-            if (existingStep == null) {
-                return true;
-            }
-
-            if (isConfigurationStepUpdated(existingStep, versionedStep)) {
-                return true;
-            }
-        }
-
-        return false;
-    }
-
-    private boolean isConfigurationStepUpdated(final NamedStepConfiguration 
existingStep, final VersionedConfigurationStep versionedStep) {
-        final Map<String, ConnectorValueReference> existingProperties = 
existingStep.configuration().getPropertyValues();
-
-        final Map<String, VersionedConnectorValueReference> 
versionedProperties = versionedStep.getProperties();
-        if (versionedProperties == null || versionedProperties.isEmpty()) {
-            return existingProperties != null && !existingProperties.isEmpty();
-        }
-
-        if (existingProperties == null || existingProperties.size() != 
versionedProperties.size()) {
-            return true;
-        }
-
-        for (final Map.Entry<String, VersionedConnectorValueReference> 
versionedEntry : versionedProperties.entrySet()) {
-            final String propertyName = versionedEntry.getKey();
-            final VersionedConnectorValueReference versionedRef = 
versionedEntry.getValue();
-            final ConnectorValueReference existingRef = 
existingProperties.get(propertyName);
-
-            if (!equals(versionedRef, existingRef)) {
-                return true;
-            }
-        }
-
-        return false;
-    }
-
-    private boolean equals(final VersionedConnectorValueReference 
versionedReference, final ConnectorValueReference existingReference) {
-        if (versionedReference == null && existingReference == null) {
-            return true;
-        }
-        if (versionedReference == null || existingReference == null) {
-            return false;
-        }
-
-        final String versionedValueType = versionedReference.getValueType();
-        final String existingValueType = 
existingReference.getValueType().name();
-        if (!Objects.equals(versionedValueType, existingValueType)) {
-            return false;
-        }
-
-        return switch (existingReference) {
-            case StringLiteralValue stringLiteral -> 
Objects.equals(stringLiteral.getValue(), versionedReference.getValue());
-            case AssetReference assetRef -> 
Objects.equals(assetRef.getAssetIdentifiers(), 
versionedReference.getAssetIds());
-            case SecretReference secretRef -> 
Objects.equals(secretRef.getProviderId(), versionedReference.getProviderId())
-                                              && 
Objects.equals(secretRef.getSecretName(), versionedReference.getSecretName());
-        };
-    }
-
-    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, flowController);
-    }
-
-
-    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());
-
-        try {
-            final List<VersionedConfigurationStep> activeFlowConfig = 
versionedConnector.getActiveFlowConfiguration();
-            final List<VersionedConfigurationStep> workingFlowConfig = 
versionedConnector.getWorkingFlowConfiguration();
-            connectorRepository.inheritConfiguration(connectorNode, 
activeFlowConfig, workingFlowConfig, versionedConnector.getBundle());
-
-            final ScheduledState desiredState = 
versionedConnector.getScheduledState();
-            if (desiredState == ScheduledState.RUNNING) {
-                flowController.startConnector(connectorNode);
-            } else if (desiredState == ScheduledState.ENABLED) {
-                connectorRepository.stopConnector(connectorNode);
+                logger.info("Connector [{}] (state={}) is no longer part of 
the proposed flow. Stopping and removing.",
+                        existingConnector.getIdentifier(), 
existingConnector.getCurrentState());
+                try {
+                    logger.debug("Stopping orphan connector [{}] before 
removal", existingConnector.getIdentifier());
+                    connectorRepository.stopConnector(existingConnector).get();
+                    logger.debug("Orphan connector [{}] stopped (state={}); 
purging data before removal",
+                            existingConnector.getIdentifier(), 
existingConnector.getCurrentState());
+                    existingConnector.purgeFlowFiles("Flow 
Synchronization").get();
+                    logger.debug("Orphan connector [{}] purged; proceeding 
with removal", existingConnector.getIdentifier());
+                    
connectorRepository.removeConnector(existingConnector.getIdentifier());
+                    logger.info("Successfully removed orphan connector [{}]", 
existingConnector.getIdentifier());
+                } catch (final Exception e) {
+                    logger.error("Failed to remove Connector [{}] during flow 
inheritance. Connector will be marked invalid.",
+                            existingConnector.getIdentifier(), e);
+                    existingConnector.markInvalid("Flow Synchronization 
Failure",
+                            "Connector could not be removed during flow sync: 
" + e.getMessage());
+                }
             }
-        } catch (final FlowUpdateException e) {
-            logger.error("{} failed to inherit configuration", connectorNode, 
e);
         }
     }
 
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/TestStandardConnectorRepository.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/TestStandardConnectorRepository.java
index 4089b274819..f6e50200852 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/TestStandardConnectorRepository.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/TestStandardConnectorRepository.java
@@ -19,7 +19,11 @@ package org.apache.nifi.components.connector;
 
 import org.apache.nifi.asset.Asset;
 import org.apache.nifi.asset.AssetManager;
+import org.apache.nifi.controller.flow.FlowManager;
+import org.apache.nifi.flow.Bundle;
+import org.apache.nifi.flow.ScheduledState;
 import org.apache.nifi.flow.VersionedConfigurationStep;
+import org.apache.nifi.flow.VersionedConnector;
 import org.apache.nifi.flow.VersionedConnectorValueReference;
 import org.apache.nifi.nar.ExtensionManager;
 import org.junit.jupiter.api.Test;
@@ -28,12 +32,14 @@ import org.mockito.ArgumentCaptor;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.time.Duration;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
+import java.util.concurrent.CompletableFuture;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -42,6 +48,7 @@ import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doThrow;
@@ -797,12 +804,379 @@ public class TestStandardConnectorRepository {
         verify(connector, never()).markInvalid(anyString(), anyString());
     }
 
+    // --- syncConnector tests ---
+
+    @Test
+    public void testSyncConnectorStoppedWithConfigChange() throws Exception {
+        final FlowManager flowManager = mock(FlowManager.class);
+        final StandardConnectorRepository repository = 
createRepositoryWithFlowManager(null, flowManager);
+
+        final ConnectorNode connector = 
createConnectorNodeWithEmptyWorkingConfig("connector-1", "Test Connector");
+        when(flowManager.createConnector(anyString(), eq("connector-1"), 
any(), anyBoolean(), anyBoolean())).thenReturn(connector);
+
+        final VersionedConnector versioned = 
createVersionedConnector("connector-1", "Test Connector", 
ScheduledState.RUNNING,
+                List.of(createVersionedStep("step1", Map.of("prop1", 
createStringLiteralRef("value1")))));
+
+        final ConnectorSyncResult result = repository.syncConnector(versioned);
+
+        assertEquals(ConnectorSyncResult.Outcome.SYNCED, result.getOutcome());
+        verify(connector).inheritConfiguration(any(), any(), any());
+    }
+
+    @Test
+    public void testSyncConnectorStoppedNoConfigChange() throws Exception {
+        final StandardConnectorRepository repository = 
createRepositoryWithProvider(null);
+
+        final ConnectorNode connector = 
createConnectorNodeWithEmptyWorkingConfig("connector-1", "Test Connector");
+        when(connector.getCurrentState()).thenReturn(ConnectorState.STOPPED);
+        repository.restoreConnector(connector);
+
+        final VersionedConnector versioned = 
createVersionedConnector("connector-1", "Test Connector", 
ScheduledState.ENABLED, List.of());
+
+        final ConnectorSyncResult result = repository.syncConnector(versioned);
+
+        assertEquals(ConnectorSyncResult.Outcome.SYNCED_CONFIG_UNCHANGED, 
result.getOutcome());
+        verify(connector, never()).inheritConfiguration(any(), any(), any());
+    }
+
+    @Test
+    public void testSyncConnectorRunningWithConfigChange() throws Exception {
+        final StandardConnectorRepository repository = 
createRepositoryWithProvider(null);
+
+        final ConnectorNode connector = 
createConnectorNodeWithEmptyWorkingConfig("connector-1", "Test Connector");
+        when(connector.getCurrentState()).thenReturn(ConnectorState.RUNNING);
+        repository.restoreConnector(connector);
+
+        final VersionedConnector versioned = 
createVersionedConnector("connector-1", "Test Connector", 
ScheduledState.RUNNING,
+                List.of(createVersionedStep("step1", Map.of("prop1", 
createStringLiteralRef("new-value")))));
+
+        final ConnectorSyncResult result = repository.syncConnector(versioned);
+
+        assertEquals(ConnectorSyncResult.Outcome.SYNCED, result.getOutcome());
+        verify(connector).inheritConfiguration(any(), any(), any());
+    }
+
+    @Test
+    public void testSyncConnectorDrainingIsRejected() throws Exception {
+        final StandardConnectorRepository repository = 
createRepositoryWithProvider(null);
+
+        final ConnectorNode connector = 
createConnectorNodeWithEmptyWorkingConfig("connector-1", "Test Connector");
+        when(connector.getCurrentState()).thenReturn(ConnectorState.DRAINING);
+        repository.restoreConnector(connector);
+
+        final VersionedConnector versioned = 
createVersionedConnector("connector-1", "Test Connector", 
ScheduledState.ENABLED, List.of());
+
+        final ConnectorSyncResult result = repository.syncConnector(versioned);
+
+        assertEquals(ConnectorSyncResult.Outcome.REJECTED, 
result.getOutcome());
+        verify(connector).markInvalid(eq("Flow Synchronization Failure"), 
anyString());
+        verify(connector, never()).inheritConfiguration(any(), any(), any());
+    }
+
+    @Test
+    public void testSyncConnectorPreparingForUpdateIsRejected() throws 
Exception {
+        final StandardConnectorRepository repository = 
createRepositoryWithProvider(null);
+
+        final ConnectorNode connector = 
createConnectorNodeWithEmptyWorkingConfig("connector-1", "Test Connector");
+        
when(connector.getCurrentState()).thenReturn(ConnectorState.PREPARING_FOR_UPDATE);
+        repository.restoreConnector(connector);
+
+        final VersionedConnector versioned = 
createVersionedConnector("connector-1", "Test Connector", 
ScheduledState.ENABLED, List.of());
+
+        final ConnectorSyncResult result = repository.syncConnector(versioned);
+
+        assertEquals(ConnectorSyncResult.Outcome.REJECTED, 
result.getOutcome());
+        verify(connector).markInvalid(eq("Flow Synchronization Failure"), 
anyString());
+    }
+
+    @Test
+    public void testSyncConnectorUpdatingIsRejected() throws Exception {
+        final StandardConnectorRepository repository = 
createRepositoryWithProvider(null);
+
+        final ConnectorNode connector = 
createConnectorNodeWithEmptyWorkingConfig("connector-1", "Test Connector");
+        when(connector.getCurrentState()).thenReturn(ConnectorState.UPDATING);
+        repository.restoreConnector(connector);
+
+        final VersionedConnector versioned = 
createVersionedConnector("connector-1", "Test Connector", 
ScheduledState.ENABLED, List.of());
+
+        final ConnectorSyncResult result = repository.syncConnector(versioned);
+
+        assertEquals(ConnectorSyncResult.Outcome.REJECTED, 
result.getOutcome());
+    }
+
+    @Test
+    public void testSyncConnectorUpdateFailedRecovery() throws Exception {
+        final StandardConnectorRepository repository = 
createRepositoryWithProvider(null);
+
+        final ConnectorNode connector = 
createConnectorNodeWithEmptyWorkingConfig("connector-1", "Test Connector");
+        
when(connector.getCurrentState()).thenReturn(ConnectorState.UPDATE_FAILED);
+        repository.restoreConnector(connector);
+
+        final VersionedConnector versioned = 
createVersionedConnector("connector-1", "Test Connector", 
ScheduledState.ENABLED,
+                List.of(createVersionedStep("step1", Map.of("prop1", 
createStringLiteralRef("recovery-value")))));
+
+        final ConnectorSyncResult result = repository.syncConnector(versioned);
+
+        assertEquals(ConnectorSyncResult.Outcome.SYNCED, result.getOutcome());
+        verify(connector).inheritConfiguration(any(), any(), any());
+    }
+
+    @Test
+    public void testSyncConnectorUpdatedState() throws Exception {
+        final FlowManager flowManager = mock(FlowManager.class);
+        final StandardConnectorRepository repository = 
createRepositoryWithFlowManager(null, flowManager);
+
+        final ConnectorNode connector = 
createConnectorNodeWithEmptyWorkingConfig("connector-1", "Test Connector");
+        when(flowManager.createConnector(anyString(), eq("connector-1"), 
any(), anyBoolean(), anyBoolean())).thenReturn(connector);
+
+        final VersionedConnector versioned = 
createVersionedConnector("connector-1", "Test Connector", 
ScheduledState.RUNNING,
+                List.of(createVersionedStep("step1", Map.of("prop1", 
createStringLiteralRef("value1")))));
+
+        final ConnectorSyncResult result = repository.syncConnector(versioned);
+
+        assertEquals(ConnectorSyncResult.Outcome.SYNCED, result.getOutcome());
+    }
+
+    @Test
+    public void testSyncConnectorInheritConfigurationFailureWhenRunning() 
throws Exception {
+        final StandardConnectorRepository repository = 
createRepositoryWithProvider(null);
+
+        final ConnectorNode connector = 
createConnectorNodeWithEmptyWorkingConfig("connector-1", "Test Connector");
+        when(connector.getCurrentState()).thenReturn(ConnectorState.RUNNING);
+        doThrow(new FlowUpdateException("Simulated failure"))
+                .when(connector).inheritConfiguration(any(), any(), any());
+        repository.restoreConnector(connector);
+
+        final VersionedConnector versioned = 
createVersionedConnector("connector-1", "Test Connector", 
ScheduledState.RUNNING,
+                List.of(createVersionedStep("step1", Map.of("prop1", 
createStringLiteralRef("value1")))));
+
+        final ConnectorSyncResult result = repository.syncConnector(versioned);
+
+        assertEquals(ConnectorSyncResult.Outcome.FAILED, result.getOutcome());
+        verify(connector).stop(any());
+    }
+
+    @Test
+    public void testSyncConnectorInheritConfigurationFailureWhenStopped() 
throws Exception {
+        final StandardConnectorRepository repository = 
createRepositoryWithProvider(null);
+
+        final ConnectorNode connector = 
createConnectorNodeWithEmptyWorkingConfig("connector-1", "Test Connector");
+        when(connector.getCurrentState()).thenReturn(ConnectorState.STOPPED);
+        doThrow(new FlowUpdateException("Simulated failure"))
+                .when(connector).inheritConfiguration(any(), any(), any());
+        repository.restoreConnector(connector);
+
+        final VersionedConnector versioned = 
createVersionedConnector("connector-1", "Test Connector", 
ScheduledState.RUNNING,
+                List.of(createVersionedStep("step1", Map.of("prop1", 
createStringLiteralRef("value1")))));
+
+        final ConnectorSyncResult result = repository.syncConnector(versioned);
+
+        assertEquals(ConnectorSyncResult.Outcome.FAILED, result.getOutcome());
+        verify(connector, never()).stop(any());
+    }
+
+    @Test
+    public void testSyncConnectorProviderRejectsSync() throws Exception {
+        final ConnectorConfigurationProvider provider = 
mock(ConnectorConfigurationProvider.class);
+        when(provider.getSyncDirective(eq("connector-1"), 
any())).thenReturn(ConnectorSyncDirective.reject());
+        final StandardConnectorRepository repository = 
createRepositoryWithProvider(provider);
+
+        final ConnectorNode connector = 
createConnectorNodeWithEmptyWorkingConfig("connector-1", "Test Connector");
+        when(connector.getCurrentState()).thenReturn(ConnectorState.STOPPED);
+        repository.restoreConnector(connector);
+
+        final VersionedConnector versioned = 
createVersionedConnector("connector-1", "Test Connector", 
ScheduledState.ENABLED, List.of());
+
+        final ConnectorSyncResult result = repository.syncConnector(versioned);
+
+        assertEquals(ConnectorSyncResult.Outcome.REJECTED, 
result.getOutcome());
+        verify(connector).markInvalid(eq("Flow Synchronization Failure"), 
anyString());
+        verify(connector, never()).inheritConfiguration(any(), any(), any());
+    }
+
+    @Test
+    public void testSyncConnectorProviderThrowsException() throws Exception {
+        final ConnectorConfigurationProvider provider = 
mock(ConnectorConfigurationProvider.class);
+        when(provider.getSyncDirective(eq("connector-1"), any()))
+                .thenThrow(new 
ConnectorConfigurationProviderException("Provider error"));
+        final StandardConnectorRepository repository = 
createRepositoryWithProvider(provider);
+
+        final ConnectorNode connector = 
createConnectorNodeWithEmptyWorkingConfig("connector-1", "Test Connector");
+        when(connector.getCurrentState()).thenReturn(ConnectorState.STOPPED);
+        repository.restoreConnector(connector);
+
+        final VersionedConnector versioned = 
createVersionedConnector("connector-1", "Test Connector", 
ScheduledState.ENABLED, List.of());
+
+        final ConnectorSyncResult result = repository.syncConnector(versioned);
+
+        assertEquals(ConnectorSyncResult.Outcome.FAILED, result.getOutcome());
+        verify(connector).markInvalid(eq("Flow Synchronization Failure"), 
anyString());
+    }
+
+    @Test
+    public void testSyncConnectorStartingWaitsForRunning() throws Exception {
+        final StandardConnectorRepository repository = 
createRepositoryWithShortTimeout(null);
+
+        final ConnectorNode connector = 
createConnectorNodeWithEmptyWorkingConfig("connector-1", "Test Connector");
+        when(connector.getCurrentState())
+                .thenReturn(ConnectorState.STARTING)
+                .thenReturn(ConnectorState.STARTING)
+                .thenReturn(ConnectorState.RUNNING);
+        repository.restoreConnector(connector);
+
+        final VersionedConnector versioned = 
createVersionedConnector("connector-1", "Test Connector", 
ScheduledState.RUNNING, List.of());
+
+        final ConnectorSyncResult result = repository.syncConnector(versioned);
+
+        assertEquals(ConnectorSyncResult.Outcome.SYNCED_CONFIG_UNCHANGED, 
result.getOutcome());
+    }
+
+    @Test
+    public void testSyncConnectorStartingTimesOut() throws Exception {
+        final StandardConnectorRepository repository = 
createRepositoryWithShortTimeout(null);
+
+        final ConnectorNode connector = 
createConnectorNodeWithEmptyWorkingConfig("connector-1", "Test Connector");
+        when(connector.getCurrentState()).thenReturn(ConnectorState.STARTING);
+        repository.restoreConnector(connector);
+
+        final VersionedConnector versioned = 
createVersionedConnector("connector-1", "Test Connector", 
ScheduledState.RUNNING, List.of());
+
+        final ConnectorSyncResult result = repository.syncConnector(versioned);
+
+        assertEquals(ConnectorSyncResult.Outcome.REJECTED, 
result.getOutcome());
+        verify(connector).markInvalid(eq("Flow Synchronization Failure"), 
anyString());
+    }
+
+    @Test
+    public void testSyncConnectorProviderReturnsRemoveStopsAndRemoves() throws 
Exception {
+        final ConnectorConfigurationProvider provider = 
mock(ConnectorConfigurationProvider.class);
+        when(provider.getSyncDirective(eq("connector-1"), 
any())).thenReturn(ConnectorSyncDirective.remove());
+        final StandardConnectorRepository repository = 
createRepositoryWithProvider(provider);
+
+        final Connector mockExtension = mock(Connector.class);
+        final ConnectorNode connector = 
createConnectorNodeWithEmptyWorkingConfig("connector-1", "Test Connector");
+        when(connector.getCurrentState())
+                .thenReturn(ConnectorState.RUNNING)
+                .thenReturn(ConnectorState.RUNNING)
+                .thenReturn(ConnectorState.STOPPED);
+        when(connector.getConnector()).thenReturn(mockExtension);
+        
when(connector.stop(any())).thenReturn(CompletableFuture.completedFuture(null));
+        repository.restoreConnector(connector);
+
+        final VersionedConnector versioned = 
createVersionedConnector("connector-1", "Test Connector", 
ScheduledState.ENABLED, List.of());
+
+        final ConnectorSyncResult result = repository.syncConnector(versioned);
+
+        assertEquals(ConnectorSyncResult.Outcome.REMOVED, result.getOutcome());
+        verify(connector).stop(any());
+        verify(connector).verifyCanDelete();
+    }
+
+    @Test
+    public void testSyncConnectorProviderReturnsRemoveStoppedConnector() 
throws Exception {
+        final ConnectorConfigurationProvider provider = 
mock(ConnectorConfigurationProvider.class);
+        when(provider.getSyncDirective(eq("connector-1"), 
any())).thenReturn(ConnectorSyncDirective.remove());
+        final StandardConnectorRepository repository = 
createRepositoryWithProvider(provider);
+
+        final Connector mockExtension = mock(Connector.class);
+        final ConnectorNode connector = 
createConnectorNodeWithEmptyWorkingConfig("connector-1", "Test Connector");
+        when(connector.getCurrentState()).thenReturn(ConnectorState.STOPPED);
+        when(connector.getConnector()).thenReturn(mockExtension);
+        repository.restoreConnector(connector);
+
+        final VersionedConnector versioned = 
createVersionedConnector("connector-1", "Test Connector", 
ScheduledState.ENABLED, List.of());
+
+        final ConnectorSyncResult result = repository.syncConnector(versioned);
+
+        assertEquals(ConnectorSyncResult.Outcome.REMOVED, result.getOutcome());
+        verify(connector, never()).stop(any());
+        verify(connector).verifyCanDelete();
+    }
+
+    @Test
+    public void testSyncConnectorProviderReturnsRemoveForNonExistent() throws 
Exception {
+        final ConnectorConfigurationProvider provider = 
mock(ConnectorConfigurationProvider.class);
+        when(provider.getSyncDirective(eq("connector-1"), 
any())).thenReturn(ConnectorSyncDirective.remove());
+        final StandardConnectorRepository repository = 
createRepositoryWithProvider(provider);
+
+        final VersionedConnector versioned = 
createVersionedConnector("connector-1", "Test Connector", 
ScheduledState.ENABLED, List.of());
+
+        final ConnectorSyncResult result = repository.syncConnector(versioned);
+
+        assertEquals(ConnectorSyncResult.Outcome.REMOVED, result.getOutcome());
+    }
+
+    @Test
+    public void 
testSyncConnectorProviderReturnsRejectCreatesNodeForNewConnector() throws 
Exception {
+        final ConnectorConfigurationProvider provider = 
mock(ConnectorConfigurationProvider.class);
+        when(provider.getSyncDirective(eq("connector-1"), 
any())).thenReturn(ConnectorSyncDirective.reject());
+
+        final FlowManager flowManager = mock(FlowManager.class);
+        final StandardConnectorRepository repository = 
createRepositoryWithFlowManagerAndProvider(provider, flowManager);
+
+        final ConnectorNode connector = 
createConnectorNodeWithEmptyWorkingConfig("connector-1", "Test Connector");
+        when(flowManager.createConnector(anyString(), eq("connector-1"), 
any(), anyBoolean(), anyBoolean())).thenReturn(connector);
+
+        final VersionedConnector versioned = 
createVersionedConnector("connector-1", "Test Connector", 
ScheduledState.ENABLED, List.of());
+
+        final ConnectorSyncResult result = repository.syncConnector(versioned);
+
+        assertEquals(ConnectorSyncResult.Outcome.REJECTED, 
result.getOutcome());
+        verify(flowManager).createConnector(anyString(), eq("connector-1"), 
any(), anyBoolean(), anyBoolean());
+        verify(connector).markInvalid(eq("Flow Synchronization Failure"), 
anyString());
+    }
+
+    @Test
+    public void testSyncConnectorProviderAllowWithScheduledStateOverride() 
throws Exception {
+        final ConnectorConfigurationProvider provider = 
mock(ConnectorConfigurationProvider.class);
+        when(provider.getSyncDirective(eq("connector-1"), any()))
+                .thenReturn(ConnectorSyncDirective.allow(null, 
ScheduledState.ENABLED));
+        final StandardConnectorRepository repository = 
createRepositoryWithProvider(provider);
+
+        final ConnectorNode connector = 
createConnectorNodeWithEmptyWorkingConfig("connector-1", "Test Connector");
+        when(connector.getCurrentState()).thenReturn(ConnectorState.STOPPED);
+        repository.restoreConnector(connector);
+
+        final VersionedConnector versioned = 
createVersionedConnector("connector-1", "Test Connector", 
ScheduledState.RUNNING, List.of());
+
+        final ConnectorSyncResult result = repository.syncConnector(versioned);
+
+        assertEquals(ConnectorSyncResult.Outcome.SYNCED_CONFIG_UNCHANGED, 
result.getOutcome());
+        assertEquals(ScheduledState.ENABLED, 
result.getEffectiveScheduledState());
+    }
+
+    @Test
+    public void testSyncConnectorProviderAllowWithWorkingConfig() throws 
Exception {
+        final ConnectorConfigurationProvider provider = 
mock(ConnectorConfigurationProvider.class);
+
+        final ConnectorWorkingConfiguration providerConfig = new 
ConnectorWorkingConfiguration();
+        providerConfig.setName("Provider Name");
+        providerConfig.setWorkingFlowConfiguration(List.of(
+                createVersionedStep("step1", Map.of("prop1", 
createStringLiteralRef("provider-value")))));
+        when(provider.getSyncDirective(eq("connector-1"), any()))
+                .thenReturn(ConnectorSyncDirective.allow(providerConfig));
+        final StandardConnectorRepository repository = 
createRepositoryWithProvider(provider);
+
+        final ConnectorNode connector = 
createConnectorNodeWithEmptyWorkingConfig("connector-1", "Test Connector");
+        when(connector.getCurrentState()).thenReturn(ConnectorState.STOPPED);
+        repository.restoreConnector(connector);
+
+        final VersionedConnector versioned = 
createVersionedConnector("connector-1", "Test Connector", 
ScheduledState.ENABLED, List.of());
+
+        final ConnectorSyncResult result = repository.syncConnector(versioned);
+
+        assertEquals(ConnectorSyncResult.Outcome.SYNCED, result.getOutcome());
+        verify(connector).setName("Provider Name");
+        verify(connector).inheritConfiguration(any(), any(), any());
+    }
+
     // --- Helper Methods ---
 
     private StandardConnectorRepository 
createRepositoryWithProviderAndAssetManager(
             final ConnectorConfigurationProvider provider, final AssetManager 
assetManager) {
         final StandardConnectorRepository repository = new 
StandardConnectorRepository();
         final ConnectorRepositoryInitializationContext initContext = 
mock(ConnectorRepositoryInitializationContext.class);
+        when(initContext.getFlowManager()).thenReturn(mock(FlowManager.class));
         
when(initContext.getExtensionManager()).thenReturn(mock(ExtensionManager.class));
         when(initContext.getAssetManager()).thenReturn(assetManager);
         
when(initContext.getConnectorConfigurationProvider()).thenReturn(provider);
@@ -811,8 +1185,13 @@ public class TestStandardConnectorRepository {
     }
 
     private StandardConnectorRepository createRepositoryWithProvider(final 
ConnectorConfigurationProvider provider) {
+        return createRepositoryWithFlowManager(provider, 
mock(FlowManager.class));
+    }
+
+    private StandardConnectorRepository createRepositoryWithFlowManager(final 
ConnectorConfigurationProvider provider, final FlowManager flowManager) {
         final StandardConnectorRepository repository = new 
StandardConnectorRepository();
         final ConnectorRepositoryInitializationContext initContext = 
mock(ConnectorRepositoryInitializationContext.class);
+        when(initContext.getFlowManager()).thenReturn(flowManager);
         
when(initContext.getExtensionManager()).thenReturn(mock(ExtensionManager.class));
         
when(initContext.getAssetManager()).thenReturn(mock(AssetManager.class));
         
when(initContext.getConnectorConfigurationProvider()).thenReturn(provider);
@@ -820,6 +1199,11 @@ public class TestStandardConnectorRepository {
         return repository;
     }
 
+    private StandardConnectorRepository 
createRepositoryWithFlowManagerAndProvider(
+            final ConnectorConfigurationProvider provider, final FlowManager 
flowManager) {
+        return createRepositoryWithFlowManager(provider, flowManager);
+    }
+
     private ConnectorNode createSimpleConnectorNode(final String id, final 
String name) {
         final ConnectorNode connector = mock(ConnectorNode.class);
         when(connector.getIdentifier()).thenReturn(id);
@@ -874,4 +1258,33 @@ public class TestStandardConnectorRepository {
         ref.setValue(value);
         return ref;
     }
+
+    private VersionedConnector createVersionedConnector(final String id, final 
String name, final ScheduledState scheduledState,
+                                                        final 
List<VersionedConfigurationStep> activeConfig) {
+        final VersionedConnector vc = new VersionedConnector();
+        vc.setInstanceIdentifier(id);
+        vc.setName(name);
+        vc.setScheduledState(scheduledState);
+        vc.setActiveFlowConfiguration(activeConfig);
+        vc.setWorkingFlowConfiguration(activeConfig);
+        final Bundle bundle = new Bundle();
+        bundle.setGroup("org.apache.nifi");
+        bundle.setArtifact("nifi-test-connector-nar");
+        bundle.setVersion("2.0.0");
+        vc.setBundle(bundle);
+        vc.setType("org.apache.nifi.test.TestConnector");
+        return vc;
+    }
+
+    private StandardConnectorRepository createRepositoryWithShortTimeout(final 
ConnectorConfigurationProvider provider) {
+        final StandardConnectorRepository repository = new 
StandardConnectorRepository();
+        final ConnectorRepositoryInitializationContext initContext = 
mock(ConnectorRepositoryInitializationContext.class);
+        when(initContext.getFlowManager()).thenReturn(mock(FlowManager.class));
+        
when(initContext.getExtensionManager()).thenReturn(mock(ExtensionManager.class));
+        
when(initContext.getAssetManager()).thenReturn(mock(AssetManager.class));
+        
when(initContext.getConnectorConfigurationProvider()).thenReturn(provider);
+        
when(initContext.getConnectorSyncTimeout()).thenReturn(Duration.ofSeconds(5));
+        repository.initialize(initContext);
+        return repository;
+    }
 }
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 71173f8a89f..47b5c2c02c1 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
@@ -20,6 +20,7 @@ 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.components.connector.ConnectorSyncResult;
 import org.apache.nifi.controller.FlowController;
 import org.apache.nifi.controller.ReportingTaskNode;
 import org.apache.nifi.controller.SnippetManager;
@@ -293,7 +294,7 @@ class VersionedFlowSynchronizerTest {
     }
 
     @Test
-    void testSyncInheritConnectorStartRoutedThroughFlowController() {
+    void testSyncInheritConnectorDelegatesSyncToConnectorRepository() {
         setRootGroup();
 
         final String connectorId = UUID.randomUUID().toString();
@@ -307,16 +308,17 @@ class VersionedFlowSynchronizerTest {
         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);
+        final ConnectorNode syncedNode = mock(ConnectorNode.class);
+        when(connectorRepository.syncConnector(versionedConnector))
+                .thenReturn(ConnectorSyncResult.synced(syncedNode, 
ScheduledState.RUNNING));
 
         setFlowController(connectorRepository);
         
when(versionedDataflow.getConnectors()).thenReturn(List.of(versionedConnector));
 
         versionedFlowSynchronizer.sync(flowController, dataFlow, flowService, 
BundleUpdateStrategy.USE_SPECIFIED_OR_GHOST);
 
-        verify(flowController).startConnector(connectorNode);
+        verify(connectorRepository).syncConnector(versionedConnector);
+        verify(flowController).startConnector(syncedNode);
     }
 
     @Test
@@ -334,16 +336,123 @@ class VersionedFlowSynchronizerTest {
         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);
+        final ConnectorNode syncedNode = mock(ConnectorNode.class);
+        when(connectorRepository.syncConnector(versionedConnector))
+                
.thenReturn(ConnectorSyncResult.syncedConfigUnchanged(syncedNode, 
ScheduledState.ENABLED));
 
         setFlowController(connectorRepository);
         
when(versionedDataflow.getConnectors()).thenReturn(List.of(versionedConnector));
 
         versionedFlowSynchronizer.sync(flowController, dataFlow, flowService, 
BundleUpdateStrategy.USE_SPECIFIED_OR_GHOST);
 
+        verify(connectorRepository).syncConnector(versionedConnector);
         verify(flowController, never()).startConnector(any());
-        verify(connectorRepository).stopConnector(connectorNode);
+        verify(connectorRepository).stopConnector(syncedNode);
+    }
+
+    @Test
+    void testSyncOrphanConnectorIsRemoved() {
+        setRootGroup();
+
+        final ConnectorRepository connectorRepository = 
mock(ConnectorRepository.class);
+
+        // The proposed flow has one connector, but the local repo has a 
different one (orphan)
+        final VersionedConnector proposedConnector = new VersionedConnector();
+        proposedConnector.setInstanceIdentifier("proposed-connector-id");
+        proposedConnector.setName("Proposed Connector");
+        proposedConnector.setType("org.apache.nifi.connectors.TestConnector");
+        proposedConnector.setBundle(CORE_BUNDLE);
+        proposedConnector.setScheduledState(ScheduledState.ENABLED);
+
+        final ConnectorNode orphanConnector = mock(ConnectorNode.class);
+        
org.mockito.Mockito.lenient().when(orphanConnector.getIdentifier()).thenReturn("orphan-connector-id");
+        when(connectorRepository.stopConnector(orphanConnector))
+                
.thenReturn(java.util.concurrent.CompletableFuture.completedFuture(null));
+
+        final ConnectorNode syncedNode = mock(ConnectorNode.class);
+        when(connectorRepository.syncConnector(proposedConnector))
+                
.thenReturn(ConnectorSyncResult.syncedConfigUnchanged(syncedNode, 
ScheduledState.ENABLED));
+        when(connectorRepository.stopConnector(syncedNode))
+                
.thenReturn(java.util.concurrent.CompletableFuture.completedFuture(null));
+
+        setFlowController(connectorRepository);
+        
when(connectorRepository.getConnectors()).thenReturn(List.of(orphanConnector));
+        
when(versionedDataflow.getConnectors()).thenReturn(List.of(proposedConnector));
+
+        versionedFlowSynchronizer.sync(flowController, dataFlow, flowService, 
BundleUpdateStrategy.USE_SPECIFIED_OR_GHOST);
+
+        verify(connectorRepository).syncConnector(proposedConnector);
+        verify(connectorRepository).stopConnector(orphanConnector);
+        verify(connectorRepository).removeConnector("orphan-connector-id");
+    }
+
+    @Test
+    void testSyncOrphanConnectorNotRemovedWhenInProposedFlow() {
+        setRootGroup();
+
+        final String connectorId = UUID.randomUUID().toString();
+        final ConnectorRepository connectorRepository = 
mock(ConnectorRepository.class);
+
+        final ConnectorNode existingConnector = mock(ConnectorNode.class);
+        
org.mockito.Mockito.lenient().when(existingConnector.getIdentifier()).thenReturn(connectorId);
+
+        final VersionedConnector versionedConnector = new VersionedConnector();
+        versionedConnector.setInstanceIdentifier(connectorId);
+        versionedConnector.setName("Test Connector");
+        versionedConnector.setType("org.apache.nifi.connectors.TestConnector");
+        versionedConnector.setBundle(CORE_BUNDLE);
+        versionedConnector.setScheduledState(ScheduledState.ENABLED);
+
+        final ConnectorNode syncedNode = mock(ConnectorNode.class);
+        when(connectorRepository.syncConnector(versionedConnector))
+                
.thenReturn(ConnectorSyncResult.syncedConfigUnchanged(syncedNode, 
ScheduledState.ENABLED));
+        when(connectorRepository.stopConnector(syncedNode))
+                
.thenReturn(java.util.concurrent.CompletableFuture.completedFuture(null));
+
+        setFlowController(connectorRepository);
+        
when(connectorRepository.getConnectors()).thenReturn(List.of(existingConnector));
+        
when(versionedDataflow.getConnectors()).thenReturn(List.of(versionedConnector));
+
+        versionedFlowSynchronizer.sync(flowController, dataFlow, flowService, 
BundleUpdateStrategy.USE_SPECIFIED_OR_GHOST);
+
+        verify(connectorRepository, never()).removeConnector(any());
+        verify(connectorRepository).syncConnector(versionedConnector);
+    }
+
+    @Test
+    void testSyncOrphanRemovalFailureMarksInvalid() {
+        setRootGroup();
+
+        final ConnectorRepository connectorRepository = 
mock(ConnectorRepository.class);
+
+        // The proposed flow has one connector; the orphan is a different one
+        final VersionedConnector proposedConnector = new VersionedConnector();
+        proposedConnector.setInstanceIdentifier("proposed-connector-id");
+        proposedConnector.setName("Proposed Connector");
+        proposedConnector.setType("org.apache.nifi.connectors.TestConnector");
+        proposedConnector.setBundle(CORE_BUNDLE);
+        proposedConnector.setScheduledState(ScheduledState.ENABLED);
+
+        final ConnectorNode orphanConnector = mock(ConnectorNode.class);
+        
org.mockito.Mockito.lenient().when(orphanConnector.getIdentifier()).thenReturn("orphan-connector-id");
+
+        final java.util.concurrent.CompletableFuture<Void> failedFuture = new 
java.util.concurrent.CompletableFuture<>();
+        failedFuture.completeExceptionally(new RuntimeException("Stop 
failed"));
+        
when(connectorRepository.stopConnector(orphanConnector)).thenReturn(failedFuture);
+
+        final ConnectorNode syncedNode = mock(ConnectorNode.class);
+        when(connectorRepository.syncConnector(proposedConnector))
+                
.thenReturn(ConnectorSyncResult.syncedConfigUnchanged(syncedNode, 
ScheduledState.ENABLED));
+        when(connectorRepository.stopConnector(syncedNode))
+                
.thenReturn(java.util.concurrent.CompletableFuture.completedFuture(null));
+
+        setFlowController(connectorRepository);
+        
when(connectorRepository.getConnectors()).thenReturn(List.of(orphanConnector));
+        
when(versionedDataflow.getConnectors()).thenReturn(List.of(proposedConnector));
+
+        versionedFlowSynchronizer.sync(flowController, dataFlow, flowService, 
BundleUpdateStrategy.USE_SPECIFIED_OR_GHOST);
+
+        verify(connectorRepository).syncConnector(proposedConnector);
+        verify(orphanConnector).markInvalid(eq("Flow Synchronization 
Failure"), any());
     }
 }
diff --git 
a/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/connectors/ClusteredConnectorIT.java
 
b/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/connectors/ClusteredConnectorIT.java
index b07646dd179..cc22e9fa271 100644
--- 
a/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/connectors/ClusteredConnectorIT.java
+++ 
b/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/connectors/ClusteredConnectorIT.java
@@ -201,6 +201,88 @@ public class ClusteredConnectorIT extends ConnectorCrudIT {
     }
 
 
+    @Test
+    public void testRunningConnectorSyncedOnReconnect() throws 
NiFiClientException, IOException, InterruptedException {
+        // Create and start a connector
+        final ConnectorEntity connector = 
getClientUtil().createConnector("NopConnector");
+        assertNotNull(connector);
+        getClientUtil().configureConnector(connector, "Ignored Step", 
Map.of("Ignored Property", "Test Value"));
+        getClientUtil().applyConnectorUpdate(connector);
+        getClientUtil().startConnector(connector);
+        logger.info("Created and started connector [{}]", connector.getId());
+
+        // Verify running on both nodes
+        switchClientToNode(1);
+        ConnectorEntity node1Connector = 
getNifiClient().getConnectorClient(DO_NOT_REPLICATE).getConnector(connector.getId());
+        assertEquals("RUNNING", node1Connector.getComponent().getState());
+
+        switchClientToNode(2);
+        ConnectorEntity node2Connector = 
getNifiClient().getConnectorClient(DO_NOT_REPLICATE).getConnector(connector.getId());
+        assertEquals("RUNNING", node2Connector.getComponent().getState());
+        logger.info("Verified connector is RUNNING on both nodes");
+
+        // Disconnect node 2
+        switchClientToNode(1);
+        disconnectNode(2);
+        logger.info("Disconnected Node 2");
+
+        // Reconnect node 2
+        reconnectNode(2);
+        waitForAllNodesConnected();
+        logger.info("Reconnected Node 2");
+
+        // Verify the connector is running on node 2 after rejoin
+        switchClientToNode(2);
+        waitFor(() -> {
+            try {
+                final ConnectorEntity rejoinedConnector = 
getNifiClient().getConnectorClient(DO_NOT_REPLICATE).getConnector(connector.getId());
+                return 
"RUNNING".equals(rejoinedConnector.getComponent().getState());
+            } catch (final Exception e) {
+                return false;
+            }
+        });
+
+        final ConnectorEntity finalNode2Connector = 
getNifiClient().getConnectorClient(DO_NOT_REPLICATE).getConnector(connector.getId());
+        assertEquals("RUNNING", finalNode2Connector.getComponent().getState());
+        assertActiveConfigurationValue(finalNode2Connector, "Ignored 
Property", "Test Value");
+        logger.info("Verified connector is RUNNING on Node 2 after rejoin with 
correct configuration");
+    }
+
+    @Test
+    public void testNewConnectorSyncedOnReconnect() throws 
NiFiClientException, IOException, InterruptedException {
+        // Disconnect node 2
+        disconnectNode(2);
+        logger.info("Disconnected Node 2");
+
+        // Create a connector on node 1 while node 2 is disconnected
+        final ConnectorEntity connector = 
getClientUtil().createConnector("NopConnector");
+        assertNotNull(connector);
+        getClientUtil().configureConnector(connector, "Ignored Step", 
Map.of("Ignored Property", "Created While Disconnected"));
+        getClientUtil().applyConnectorUpdate(connector);
+        logger.info("Created connector [{}] on Node 1 while Node 2 is 
disconnected", connector.getId());
+
+        // Reconnect node 2
+        reconnectNode(2);
+        waitForAllNodesConnected();
+        logger.info("Reconnected Node 2");
+
+        // Verify the connector exists on node 2 after rejoin
+        switchClientToNode(2);
+        waitFor(() -> {
+            try {
+                
getNifiClient().getConnectorClient(DO_NOT_REPLICATE).getConnector(connector.getId());
+                return true;
+            } catch (final Exception e) {
+                return false;
+            }
+        });
+
+        final ConnectorEntity node2Connector = 
getNifiClient().getConnectorClient(DO_NOT_REPLICATE).getConnector(connector.getId());
+        assertNotNull(node2Connector);
+        assertActiveConfigurationValue(node2Connector, "Ignored Property", 
"Created While Disconnected");
+        logger.info("Verified new connector exists on Node 2 after rejoin with 
correct configuration");
+    }
+
     private void assertActiveConfigurationValue(final ConnectorEntity 
connector, final String propertyName, final String expectedValue) {
         final String actualValue = 
getConfigurationValue(connector.getComponent().getActiveConfiguration(), 
propertyName);
         assertEquals(expectedValue, actualValue, "Active configuration 
property '" + propertyName + "' did not match expected value");

Reply via email to