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