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

markap14 pushed a commit to branch NIFI-15258
in repository https://gitbox.apache.org/repos/asf/nifi.git

commit dac8e8a013a1ad875f0dca80eb386c1e6f99e0b6
Author: Mark Payne <[email protected]>
AuthorDate: Wed Jan 7 16:49:07 2026 -0500

    NIFI-15430: Ensure that if we fail to initialize a Connector, we crea… 
(#10733)
    
    * NIFI-15430: Ensure that if we fail to initialize a Connector, we create a 
GhostConnector instead and ensure that we also proivde the extensionMissing 
flag on ConnectorNode
    
    * NIFI-15430: Added extensionMissing flag to Connector DTO
    
    * NIFI-15430: If unable to load initial flow of a Connector, make ghosted 
connector instead
---
 .../org/apache/nifi/web/api/dto/ConnectorDTO.java  | 10 +++
 .../nifi/components/connector/ConnectorNode.java   |  6 ++
 .../connector/StandardConnectorNode.java           |  9 ++-
 .../apache/nifi/controller/ExtensionBuilder.java   | 90 ++++++++++++++--------
 .../nifi/controller/flow/StandardFlowManager.java  | 11 +--
 .../connector/TestStandardConnectorNode.java       |  3 +-
 .../org/apache/nifi/web/api/dto/DtoFactory.java    | 19 +++--
 7 files changed, 96 insertions(+), 52 deletions(-)

diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ConnectorDTO.java
 
b/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ConnectorDTO.java
index 96849a58a0..a2253fdfeb 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ConnectorDTO.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ConnectorDTO.java
@@ -37,6 +37,7 @@ public class ConnectorDTO extends ComponentDTO {
     private Collection<String> validationErrors;
     private String validationStatus;
     private Boolean multipleVersionsAvailable;
+    private Boolean extensionMissing;
 
     private String configurationUrl;
     private String detailsUrl;
@@ -148,4 +149,13 @@ public class ConnectorDTO extends ComponentDTO {
     public void setDetailsUrl(final String detailsUrl) {
         this.detailsUrl = detailsUrl;
     }
+
+    @Schema(description = "Whether the extension for this connector is 
missing.")
+    public Boolean getExtensionMissing() {
+        return extensionMissing;
+    }
+
+    public void setExtensionMissing(final Boolean extensionMissing) {
+        this.extensionMissing = extensionMissing;
+    }
 }
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/components/connector/ConnectorNode.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/components/connector/ConnectorNode.java
index 9dd1fe36a1..b7206edbd3 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/components/connector/ConnectorNode.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/components/connector/ConnectorNode.java
@@ -66,6 +66,12 @@ public interface ConnectorNode extends 
ComponentAuthorizable, VersionedComponent
 
     BundleCoordinate getBundleCoordinate();
 
+    /**
+     * Returns whether or not the underlying extension is missing (i.e., the 
Connector is a GhostConnector).
+     * @return true if the extension is missing, false otherwise
+     */
+    boolean isExtensionMissing();
+
     List<AllowableValue> fetchAllowableValues(String stepName, String 
propertyName);
 
     List<AllowableValue> fetchAllowableValues(String stepName, String 
propertyName, String filter);
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/StandardConnectorNode.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/StandardConnectorNode.java
index 59f2dfba10..14713ecee9 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/StandardConnectorNode.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/StandardConnectorNode.java
@@ -89,6 +89,7 @@ public class StandardConnectorNode implements ConnectorNode {
 
     private final AtomicReference<ValidationState> validationState = new 
AtomicReference<>(new ValidationState(ValidationStatus.VALIDATING, 
Collections.emptyList()));
     private final ConnectorValidationTrigger validationTrigger;
+    private final boolean extensionMissing;
     private volatile boolean triggerValidation = true;
 
     private volatile FrameworkFlowContext workingFlowContext;
@@ -101,7 +102,7 @@ public class StandardConnectorNode implements ConnectorNode 
{
         final Authorizable parentAuthorizable, final ConnectorDetails 
connectorDetails, final String componentType, final String 
componentCanonicalClass,
         final MutableConnectorConfigurationContext configurationContext,
         final ConnectorStateTransition stateTransition, final 
FlowContextFactory flowContextFactory,
-        final ConnectorValidationTrigger validationTrigger) {
+        final ConnectorValidationTrigger validationTrigger, final boolean 
extensionMissing) {
 
         this.identifier = identifier;
         this.flowManager = flowManager;
@@ -114,6 +115,7 @@ public class StandardConnectorNode implements ConnectorNode 
{
         this.stateTransition = stateTransition;
         this.flowContextFactory = flowContextFactory;
         this.validationTrigger = validationTrigger;
+        this.extensionMissing = extensionMissing;
 
         this.name = connectorDetails.getConnector().getClass().getSimpleName();
 
@@ -531,6 +533,11 @@ public class StandardConnectorNode implements 
ConnectorNode {
         return bundleCoordinate;
     }
 
+    @Override
+    public boolean isExtensionMissing() {
+        return extensionMissing;
+    }
+
     @Override
     public List<AllowableValue> fetchAllowableValues(final String stepName, 
final String propertyName) {
         if (workingFlowContext == null) {
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/ExtensionBuilder.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/ExtensionBuilder.java
index c1eac07a4c..54ed942e70 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/ExtensionBuilder.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/ExtensionBuilder.java
@@ -33,9 +33,9 @@ import org.apache.nifi.components.connector.Connector;
 import org.apache.nifi.components.connector.ConnectorDetails;
 import org.apache.nifi.components.connector.ConnectorNode;
 import org.apache.nifi.components.connector.ConnectorPropertyDescriptor;
-import org.apache.nifi.components.connector.ConnectorValidationTrigger;
 import org.apache.nifi.components.connector.ConnectorPropertyGroup;
 import org.apache.nifi.components.connector.ConnectorStateTransition;
+import org.apache.nifi.components.connector.ConnectorValidationTrigger;
 import org.apache.nifi.components.connector.ConnectorValueReference;
 import org.apache.nifi.components.connector.FlowContextFactory;
 import 
org.apache.nifi.components.connector.FrameworkConnectorInitializationContext;
@@ -476,37 +476,13 @@ public class ExtensionBuilder {
         return flowAnalysisRuleNode;
     }
 
-    public ConnectorNode buildConnector() {
+    public ConnectorNode buildConnector(final boolean loadInitialFlow) {
         requireNonNull(identifier, "Connector ID");
         requireNonNull(type, "Connector Type");
         requireNonNull(bundleCoordinate, "Bundle Coordinate");
         requireNonNull(extensionManager, "Extension Manager");
         requireNonNull(managedProcessGroup, "Managed Process Group");
 
-        boolean creationSuccessful = true;
-        Connector connector;
-
-        try {
-            connector = createConnector();
-        } catch (final Exception e) {
-            logger.error("Could not create Connector of type {} from {} for ID 
{} due to: {}; creating \"Ghost\" implementation", type, bundleCoordinate, 
identifier, e.getMessage(), e);
-            connector = new GhostConnector(identifier, type);
-            creationSuccessful = false;
-        }
-
-        final String componentType;
-        if (creationSuccessful) {
-            componentType = connector.getClass().getSimpleName();
-        } else {
-            final String simpleClassName = type.contains(".") ? 
StringUtils.substringAfterLast(type, ".") : type;
-            componentType = "(Missing) " + simpleClassName;
-        }
-
-        final ComponentLog componentLog = new SimpleProcessLogger(identifier, 
connector, new StandardLoggingContext());
-        final ConnectorDetails connectorDetails = new 
ConnectorDetails(connector, bundleCoordinate, componentLog);
-
-        final FrameworkConnectorInitializationContext initContext = 
createConnectorInitializationContext(managedProcessGroup, componentLog);
-
         final Authorizable connectorsAuthorizable = new Authorizable() {
             @Override
             public Authorizable getParentAuthorizable() {
@@ -519,6 +495,18 @@ public class ExtensionBuilder {
             }
         };
 
+        final Connector connector;
+        try {
+            connector = createConnector();
+        } catch (final Exception e) {
+            logger.error("Could not create Connector of type {} from {} for ID 
{} due to: {}; creating \"Ghost\" implementation", type, bundleCoordinate, 
identifier, e.getMessage(), e);
+            return createGhostConnectorNode(connectorsAuthorizable);
+        }
+
+        final String componentType = connector.getClass().getSimpleName();
+        final ComponentLog componentLog = new SimpleProcessLogger(identifier, 
connector, new StandardLoggingContext());
+        final ConnectorDetails connectorDetails = new 
ConnectorDetails(connector, bundleCoordinate, componentLog);
+
         final ConnectorNode connectorNode = new StandardConnectorNode(
             identifier,
             flowController.getFlowManager(),
@@ -530,16 +518,58 @@ public class ExtensionBuilder {
             activeConfigurationContext,
             connectorStateTransition,
             flowContextFactory,
-            connectorValidationTrigger
+            connectorValidationTrigger,
+            false
         );
 
-        initializeDefaultValues(connector, 
connectorNode.getActiveFlowContext());
-        // TODO: If an Exception is thrown in the call to #initialize, we 
should create a Ghosted Connector
-        connectorNode.initializeConnector(initContext);
+        try {
+            initializeDefaultValues(connector, 
connectorNode.getActiveFlowContext());
+
+            final FrameworkConnectorInitializationContext initContext = 
createConnectorInitializationContext(managedProcessGroup, componentLog);
+            connectorNode.initializeConnector(initContext);
+        } catch (final Exception e) {
+            logger.error("Could not initialize Connector of type {} from {} 
for ID {}; creating \"Ghost\" implementation", type, bundleCoordinate, 
identifier, e);
+            return createGhostConnectorNode(connectorsAuthorizable);
+        }
+
+        if (loadInitialFlow) {
+            try {
+                connectorNode.loadInitialFlow();
+            } catch (final Exception e) {
+                logger.error("Failed to load initial flow for {}; creating 
\"Ghost\" implementation", connectorNode, e);
+                return createGhostConnectorNode(connectorsAuthorizable);
+            }
+        }
 
         return connectorNode;
     }
 
+    private ConnectorNode createGhostConnectorNode(final Authorizable 
connectorsAuthorizable) {
+        final GhostConnector ghostConnector = new GhostConnector(identifier, 
type);
+        final String simpleClassName = type.contains(".") ? 
StringUtils.substringAfterLast(type, ".") : type;
+        final String componentType = "(Missing) " + simpleClassName;
+        final ComponentLog componentLog = new SimpleProcessLogger(identifier, 
ghostConnector, new StandardLoggingContext());
+        final ConnectorDetails connectorDetails = new 
ConnectorDetails(ghostConnector, bundleCoordinate, componentLog);
+
+        // If an instance class loader has been created for this connector, 
remove it because it's no longer necessary.
+        extensionManager.removeInstanceClassLoader(identifier);
+
+        return new StandardConnectorNode(
+            identifier,
+            flowController.getFlowManager(),
+            extensionManager,
+            connectorsAuthorizable,
+            connectorDetails,
+            componentType,
+            type,
+            activeConfigurationContext,
+            connectorStateTransition,
+            flowContextFactory,
+            connectorValidationTrigger,
+            true
+        );
+    }
+
     private void initializeDefaultValues(final Connector connector, final 
FrameworkFlowContext flowContext) {
         try (final NarCloseable ignored = 
NarCloseable.withComponentNarLoader(extensionManager, connector.getClass(), 
identifier)) {
             final List<ConfigurationStep> configSteps = 
connector.getConfigurationSteps();
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/flow/StandardFlowManager.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/flow/StandardFlowManager.java
index 65e8011b90..96bbd8f4f7 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/flow/StandardFlowManager.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/flow/StandardFlowManager.java
@@ -779,16 +779,7 @@ public class StandardFlowManager extends 
AbstractFlowManager implements FlowMana
             .connectorStateTransition(stateTransition)
             
.connectorInitializationContextBuilder(flowController.getConnectorRepository().createInitializationContextBuilder())
             
.connectorValidationTrigger(flowController.getConnectorValidationTrigger())
-            .buildConnector();
-
-        if (firstTimeAdded) {
-            try {
-                connectorNode.loadInitialFlow();
-            } catch (final Exception e) {
-                logger.error("Failed to load initial flow for Connector {}", 
connectorNode, e);
-                // TODO: Create a Ghosted Connector instead
-            }
-        }
+            .buildConnector(firstTimeAdded);
 
         // Establish the Connector as the parent authorizable of the managed 
root group
         managedRootGroup.setExplicitParentAuthorizable(connectorNode);
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/TestStandardConnectorNode.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/TestStandardConnectorNode.java
index 03da830e03..a1490409da 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/TestStandardConnectorNode.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/TestStandardConnectorNode.java
@@ -513,7 +513,8 @@ public class TestStandardConnectorNode {
             new StandardConnectorConfigurationContext(assetManager, 
secretsManager),
             stateTransition,
             flowContextFactory,
-            validationTrigger);
+            validationTrigger,
+            false);
 
         // mock secrets manager
         final SecretsManager secretsManager = mock(SecretsManager.class);
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
index 1fa240ced1..9e60291f13 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
@@ -66,14 +66,9 @@ import org.apache.nifi.components.ConfigVerificationResult;
 import org.apache.nifi.components.PropertyDependency;
 import org.apache.nifi.components.PropertyDescriptor;
 import org.apache.nifi.components.ValidationResult;
-import org.apache.nifi.components.connector.ConnectorAssetRepository;
-import org.apache.nifi.components.state.Scope;
-import org.apache.nifi.components.state.StateMap;
-import org.apache.nifi.components.validation.ValidationState;
-import org.apache.nifi.components.validation.ValidationStatus;
-import org.apache.nifi.connectable.Connectable;
 import org.apache.nifi.components.connector.AssetReference;
 import org.apache.nifi.components.connector.ConfigurationStep;
+import org.apache.nifi.components.connector.ConnectorAssetRepository;
 import org.apache.nifi.components.connector.ConnectorConfiguration;
 import org.apache.nifi.components.connector.ConnectorNode;
 import org.apache.nifi.components.connector.ConnectorPropertyDescriptor;
@@ -85,6 +80,11 @@ import org.apache.nifi.components.connector.Secret;
 import org.apache.nifi.components.connector.SecretReference;
 import org.apache.nifi.components.connector.StepConfiguration;
 import org.apache.nifi.components.connector.StringLiteralValue;
+import org.apache.nifi.components.state.Scope;
+import org.apache.nifi.components.state.StateMap;
+import org.apache.nifi.components.validation.ValidationState;
+import org.apache.nifi.components.validation.ValidationStatus;
+import org.apache.nifi.connectable.Connectable;
 import org.apache.nifi.connectable.ConnectableType;
 import org.apache.nifi.connectable.Connection;
 import org.apache.nifi.connectable.Funnel;
@@ -144,8 +144,8 @@ import org.apache.nifi.groups.ProcessGroupCounts;
 import org.apache.nifi.groups.RemoteProcessGroup;
 import org.apache.nifi.groups.RemoteProcessGroupCounts;
 import org.apache.nifi.history.History;
-import org.apache.nifi.nar.ExtensionDefinition;
 import org.apache.nifi.manifest.RuntimeManifestService;
+import org.apache.nifi.nar.ExtensionDefinition;
 import org.apache.nifi.nar.ExtensionManager;
 import org.apache.nifi.nar.NarClassLoadersHolder;
 import org.apache.nifi.nar.NarManifest;
@@ -5256,9 +5256,8 @@ public final class DtoFactory {
         final ValidationState validationState = connector.getValidationState();
         dto.setValidationStatus(validationState.getStatus().name());
         
dto.setValidationErrors(convertValidationErrors(validationState.getValidationErrors()));
-
-        final String canonicalName = 
connector.getConnector().getClass().getCanonicalName();
-        dto.setType(canonicalName != null ? canonicalName : 
connector.getConnector().getClass().getName());
+        dto.setType(connector.getCanonicalClassName());
+        dto.setExtensionMissing(connector.isExtensionMissing());
 
         dto.setBundle(createBundleDto(connector.getBundleCoordinate()));
         dto.setState(connector.getCurrentState().name());

Reply via email to