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

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


The following commit(s) were added to refs/heads/NIFI-15258 by this push:
     new 73a0dec49c NIFI-15430: Ensure that if we fail to initialize a 
Connector, we crea… (#10733)
73a0dec49c is described below

commit 73a0dec49cb8a8bc597b562a53de3b8ce102903f
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   | 94 ++++++++++++++--------
 .../nifi/controller/flow/StandardFlowManager.java  | 11 +--
 .../connector/TestStandardConnectorNode.java       |  3 +-
 .../org/apache/nifi/web/api/dto/DtoFactory.java    | 19 +++--
 7 files changed, 98 insertions(+), 54 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 64e4af6c72..3f16f8d25e 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
@@ -19,11 +19,11 @@ package org.apache.nifi.controller;
 import org.apache.commons.lang3.ClassUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.annotation.behavior.RequiresInstanceClassLoading;
+import org.apache.nifi.annotation.behavior.SupportsBatching;
+import org.apache.nifi.annotation.configuration.DefaultSettings;
 import org.apache.nifi.authorization.Resource;
 import org.apache.nifi.authorization.resource.Authorizable;
 import org.apache.nifi.authorization.resource.ResourceFactory;
-import org.apache.nifi.annotation.behavior.SupportsBatching;
-import org.apache.nifi.annotation.configuration.DefaultSettings;
 import org.apache.nifi.bundle.Bundle;
 import org.apache.nifi.bundle.BundleCoordinate;
 import org.apache.nifi.components.ConfigurableComponent;
@@ -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 logger = new SimpleProcessLogger(identifier, 
connector, new StandardLoggingContext());
-       final ConnectorDetails connectorDetails = new 
ConnectorDetails(connector, bundleCoordinate, logger);
-
-       final FrameworkConnectorInitializationContext initContext = 
createConnectorInitializationContext(managedProcessGroup, logger);
-
        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 9ecb1e78d2..cf889dfc2c 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