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 62964f0cdba NIFI-16060 Tolerate connector configuration load failures 
on read (#11380)
62964f0cdba is described below

commit 62964f0cdba91c5b9d84c51d0317e3d1f1b92177
Author: Kevin Doran <[email protected]>
AuthorDate: Wed Jul 1 09:22:53 2026 -0400

    NIFI-16060 Tolerate connector configuration load failures on read (#11380)
    
    * NIFI-16060 Tolerate connector configuration load failures on read
    
    When a connector's stored configuration cannot be loaded or parsed (e.g. a
    corrupt config left behind by a failed commit), the 
ConnectorConfigurationProvider
    throws ConnectorConfigurationProviderException. On the SYNC_WITH_PROVIDER 
read
    path this exception previously propagated uncaught and surfaced as an HTTP 
500
    from the connector REST endpoints. Because the delete flow reads the 
connector
    (for its revision, and to snapshot it before deletion), a corrupt connector
    became impossible to read or delete.
    
    Make the two read retrieval methods (getConnector and getConnectors with
    SYNC_WITH_PROVIDER) tolerate a configuration-load failure: log a warning, 
mark
    the connector invalid so the failure remains visible to clients via the 
DTO's
    validation status, and return the in-memory node instead of propagating. The
    connector therefore stays readable and deletable.
    
    Write paths (addConnector, applyUpdate) call syncFromProvider directly and
    continue to propagate the exception, so create/apply-config still fail on a 
bad
    configuration rather than proceeding silently.
    
    * NIFI-16060 Do not mark connector invalid on read-time config load failure
    
    A read-time configuration-load failure may be transient (e.g. a 
provider/network
    blip), not a corrupt configuration. markInvalid sets a sticky INVALID 
validation
    state that is only cleared on a config/update lifecycle event, not by a 
later
    successful read, so marking invalid on a tolerated read would leave a 
healthy
    connector permanently invalid after the underlying issue resolves.
    
    Drop the markInvalid call from the tolerant read path: log the failure and 
return
    the connector with its existing state untouched, so it recovers on the next
    successful read. Update the tests to assert the connector is never marked 
invalid.
    
    * NIFI-16060 Log connector config load failure on read at error level
    
    A configuration that cannot be loaded during a read is an operational 
problem
    worth surfacing prominently (the connector's working configuration is stale 
or
    unavailable), so log it at ERROR rather than WARN.
---
 .../connector/StandardConnectorRepository.java     | 24 +++++++++++-
 .../connector/TestStandardConnectorRepository.java | 44 +++++++++++++++++++++-
 2 files changed, 64 insertions(+), 4 deletions(-)

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 b68df8723e5..185ad7f100e 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
@@ -521,7 +521,7 @@ public class StandardConnectorRepository implements 
ConnectorRepository {
         Objects.requireNonNull(syncMode, "syncMode is required");
         final ConnectorNode connector = connectors.get(identifier);
         if (connector != null && syncMode == 
ConnectorSyncMode.SYNC_WITH_PROVIDER) {
-            syncFromProvider(connector);
+            syncFromProviderForRead(connector);
         }
         return connector;
     }
@@ -532,12 +532,32 @@ public class StandardConnectorRepository implements 
ConnectorRepository {
         final List<ConnectorNode> connectorList = 
List.copyOf(connectors.values());
         if (syncMode == ConnectorSyncMode.SYNC_WITH_PROVIDER) {
             for (final ConnectorNode connector : connectorList) {
-                syncFromProvider(connector);
+                syncFromProviderForRead(connector);
             }
         }
         return connectorList;
     }
 
+    /**
+     * Sync a connector from the provider for a read operation, tolerating a 
configuration-load failure.
+     * A configuration that cannot be loaded or parsed (e.g. a transient 
provider error, or a corrupt
+     * stored configuration left by a failed commit) must not make a connector 
unreadable — and therefore
+     * undeletable. On failure we log and return the in-memory node without 
updating its working
+     * configuration; the connector's own state is left untouched, so a 
subsequent successful read recovers
+     * normally. Write paths ({@code addConnector}, {@code applyUpdate}) call
+     * {@link #syncFromProvider(ConnectorNode)} directly and continue to 
propagate the exception so they do
+     * not proceed on a configuration that could not be loaded.
+     */
+    private void syncFromProviderForRead(final ConnectorNode connector) {
+        try {
+            syncFromProvider(connector);
+        } catch (final ConnectorConfigurationProviderException e) {
+            logger.error("Failed to load configuration from provider for 
connector [{}] during a read operation; "
+                    + "returning the connector with its existing configuration 
so it remains readable and deletable",
+                    connector.getIdentifier(), e);
+        }
+    }
+
     @Override
     public Future<Void> startConnector(final ConnectorNode connector) {
         return connector.start(lifecycleExecutor);
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 68020bb8677..7381302ff1d 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
@@ -274,7 +274,7 @@ public class TestStandardConnectorRepository {
     }
 
     @Test
-    public void testGetConnectorWithProviderThrowsException() {
+    public void testGetConnectorToleratesProviderException() {
         final ConnectorConfigurationProvider provider = 
mock(ConnectorConfigurationProvider.class);
         final StandardConnectorRepository repository = 
createRepositoryWithProvider(provider);
 
@@ -283,10 +283,50 @@ public class TestStandardConnectorRepository {
 
         when(provider.load("connector-1")).thenThrow(new 
ConnectorConfigurationProviderException("Provider failure"));
 
-        assertThrows(ConnectorConfigurationProviderException.class, () -> 
repository.getConnector("connector-1", ConnectorSyncMode.SYNC_WITH_PROVIDER));
+        // A configuration-load failure on a read must not propagate: the 
connector must remain readable
+        // (and therefore deletable). The node is returned with its existing 
state untouched -- in particular
+        // it is NOT marked invalid, so a transient failure does not leave a 
healthy connector permanently
+        // invalid (markInvalid is not cleared by a subsequent successful 
read).
+        final ConnectorNode result = repository.getConnector("connector-1", 
ConnectorSyncMode.SYNC_WITH_PROVIDER);
+
+        assertNotNull(result);
+        assertEquals(connector, result);
+        verify(connector, never()).markInvalid(anyString(), anyString());
         verify(connector, never()).setName(anyString());
     }
 
+    @Test
+    public void testGetConnectorsToleratesProviderException() {
+        final ConnectorConfigurationProvider provider = 
mock(ConnectorConfigurationProvider.class);
+        final StandardConnectorRepository repository = 
createRepositoryWithProvider(provider);
+
+        final ConnectorNode connector = 
createSimpleConnectorNode("connector-1", "Original Name");
+        repository.addConnector(connector);
+
+        when(provider.load("connector-1")).thenThrow(new 
ConnectorConfigurationProviderException("Provider failure"));
+
+        final List<ConnectorNode> results = 
repository.getConnectors(ConnectorSyncMode.SYNC_WITH_PROVIDER);
+
+        assertEquals(1, results.size());
+        assertTrue(results.contains(connector));
+        verify(connector, never()).markInvalid(anyString(), anyString());
+        verify(connector, never()).setName(anyString());
+    }
+
+    @Test
+    public void testAddConnectorPropagatesProviderException() {
+        final ConnectorConfigurationProvider provider = 
mock(ConnectorConfigurationProvider.class);
+        final StandardConnectorRepository repository = 
createRepositoryWithProvider(provider);
+
+        final ConnectorNode connector = 
createSimpleConnectorNode("connector-1", "Original Name");
+        when(provider.load("connector-1")).thenThrow(new 
ConnectorConfigurationProviderException("Provider failure"));
+
+        // Write paths must remain strict: a configuration-load failure during 
create must propagate so that
+        // create/apply-config does not silently proceed on a bad 
configuration (only reads are made tolerant).
+        assertThrows(ConnectorConfigurationProviderException.class, () -> 
repository.addConnector(connector));
+        verify(connector, never()).markInvalid(anyString(), anyString());
+    }
+
     @Test
     public void testGetConnectorWithNullProvider() {
         final StandardConnectorRepository repository = 
createRepositoryWithProvider(null);

Reply via email to