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());
