This is an automated email from the ASF dual-hosted git repository.
pvillard31 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 bc53d07bb93 NIFI-16262 Fixed Connector stop handling for multiple
callers (#11597)
bc53d07bb93 is described below
commit bc53d07bb93dda7a1b168107c0525a132364f151
Author: David Handermann <[email protected]>
AuthorDate: Mon Aug 31 04:04:51 2026 -0500
NIFI-16262 Fixed Connector stop handling for multiple callers (#11597)
---
.../connector/StandardConnectorNode.java | 41 +++++-
.../connector/TestStandardConnectorNode.java | 161 ++++++++++++++++++++-
2 files changed, 195 insertions(+), 7 deletions(-)
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 552a95f90ea..1e0ae929d4c 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
@@ -88,7 +88,6 @@ import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
-import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
@@ -737,6 +736,13 @@ public class StandardConnectorNode implements
ConnectorNode, GroupedComponent {
}
stateUpdated = stateTransition.trySetCurrentState(currentState,
ConnectorState.STOPPING);
+
+ // Check current state for existing start request in progress
+ if (stateUpdated && currentState == ConnectorState.STARTING) {
+ logger.info("{} is currently starting so will not stop the
Connector until the start has completed", this);
+ stateTransition.addPendingStopFuture(stopCompleteFuture);
+ return stopCompleteFuture;
+ }
}
scheduler.schedule(() -> stopComponent(scheduler, stopCompleteFuture),
0, TimeUnit.SECONDS);
@@ -882,25 +888,48 @@ public class StandardConnectorNode implements
ConnectorNode, GroupedComponent {
}
}
- private void startComponent(final ScheduledExecutorService scheduler,
final CompletableFuture<Void> startCompleteFuture) {
+ private void startComponent(final FlowEngine scheduler, final
CompletableFuture<Void> startCompleteFuture) {
logger.debug("Starting component for {}", this);
final ConnectorState desiredState = getDesiredState();
if (desiredState != ConnectorState.RUNNING) {
logger.info("Will not start {} because the desired state is no
longer RUNNING but is now {}", this, desiredState);
+ completeDeferredStop(scheduler);
return;
}
try (final NarCloseable ignored =
NarCloseable.withComponentNarLoader(extensionManager,
connectorDetails.getConnector().getClass(), getIdentifier())) {
connectorDetails.getConnector().start(activeFlowContext);
} catch (final Exception e) {
- logger.error("Failed to start {}. Will try again in 10 seconds",
this, e);
- scheduler.schedule(() -> startComponent(scheduler,
startCompleteFuture), 10, TimeUnit.SECONDS);
+ if (getCurrentState() == ConnectorState.STOPPING) {
+ logger.error("Failed to start {} and a stop has since been
requested, so the Connector will be stopped instead of started", this, e);
+ completeDeferredStop(scheduler);
+ } else {
+ logger.error("Failed to start {} retrying in 10 seconds",
this, e);
+ scheduler.schedule(() -> startComponent(scheduler,
startCompleteFuture), 10, TimeUnit.SECONDS);
+ }
+
return;
}
- stateTransition.setCurrentState(ConnectorState.RUNNING);
+ // A stop requested while the Connector was starting transitioned the
current state away from STARTING, so the
+ // Connector may be reported as RUNNING only if it is still STARTING.
Otherwise, this thread owns the stop that
+ // was deferred while the start was in flight.
+ final boolean transitionedToRunning =
stateTransition.trySetCurrentState(ConnectorState.STARTING,
ConnectorState.RUNNING);
startCompleteFuture.complete(null);
- logger.info("Successfully started {}", this);
+
+ if (transitionedToRunning) {
+ logger.info("Successfully started {}", this);
+ } else {
+ logger.info("Started {} but its current state is now {} so it will
not be reported as RUNNING", this, getCurrentState());
+ completeDeferredStop(scheduler);
+ }
+ }
+
+ private void completeDeferredStop(final FlowEngine scheduler) {
+ if (getCurrentState() == ConnectorState.STOPPING) {
+ logger.info("{} was requested to stop while it was starting so
will now be stopped", this);
+ stopComponent(scheduler, new CompletableFuture<>());
+ }
}
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 22a148bb709..767c20a5abc 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
@@ -85,6 +85,8 @@ import static org.mockito.Mockito.when;
public class TestStandardConnectorNode {
+ private static final long STOP_NOT_EXPECTED_MILLIS = 250L;
+
private FlowEngine scheduler;
@Mock
@@ -98,11 +100,13 @@ public class TestStandardConnectorNode {
private FlowContextFactory flowContextFactory;
private StateManagerProvider stateManagerProvider;
+ private StartBlockingConnector startBlockingConnector;
@BeforeEach
public void setUp() {
MockitoAnnotations.openMocks(this);
- scheduler = new FlowEngine(1, "flow-engine");
+ // Multiple Threads configured to support concurrent lifecycle
operations
+ scheduler = new FlowEngine(2, "flow-engine");
stateManagerProvider = new MockStateManagerProvider();
when(managedProcessGroup.purge()).thenReturn(CompletableFuture.completedFuture(null));
@@ -135,6 +139,10 @@ public class TestStandardConnectorNode {
@AfterEach
public void teardown() {
+ if (startBlockingConnector != null) {
+ startBlockingConnector.releaseStart();
+ }
+
if (scheduler != null) {
scheduler.close();
}
@@ -297,6 +305,63 @@ public class TestStandardConnectorNode {
assertTrue(startFuture.isDone());
}
+ @Test
+ @Timeout(value = 10, unit = TimeUnit.SECONDS)
+ public void testStopWhileStartingStopsConnectorOnceStartCompletes() throws
Exception {
+ final StartBlockingConnector connector =
createStartBlockingConnector();
+ final StandardConnectorNode connectorNode =
createConnectorNode(connector);
+
+ final Future<Void> startFuture = connectorNode.start(scheduler);
+ assertTrue(connector.awaitStartEntered(5, TimeUnit.SECONDS));
+ assertEquals(ConnectorState.STARTING, connectorNode.getCurrentState());
+
+ final Future<Void> stopFuture = connectorNode.stop(scheduler);
+ assertEquals(ConnectorState.STOPPING, connectorNode.getCurrentState());
+ assertEquals(ConnectorState.STOPPED, connectorNode.getDesiredState());
+
+ // The Connector must not be stopped while its start is still in
progress. The stop is carried out by the
+ // thread performing the start, once that start has finished, so the
Connector remains blocked in start and
+ // in the STOPPING state for as long as the start is held.
+ assertFalse(connector.awaitStopEntered(STOP_NOT_EXPECTED_MILLIS,
TimeUnit.MILLISECONDS));
+ assertEquals(ConnectorState.STOPPING, connectorNode.getCurrentState());
+
+ connector.releaseStart();
+
+ stopFuture.get(5, TimeUnit.SECONDS);
+ startFuture.get(5, TimeUnit.SECONDS);
+
+ // A start that finishes after a stop has been requested must not
leave the Connector reporting RUNNING.
+ assertFalse(connector.wasStoppedWhileStarting());
+ assertEquals(ConnectorState.STOPPED, connectorNode.getCurrentState());
+ assertEquals(ConnectorState.STOPPED, connectorNode.getDesiredState());
+ }
+
+ @Test
+ @Timeout(value = 10, unit = TimeUnit.SECONDS)
+ public void
testStartWhileStopIsPendingForInFlightStartLeavesConnectorRunning() throws
Exception {
+ final StartBlockingConnector connector =
createStartBlockingConnector();
+ final StandardConnectorNode connectorNode =
createConnectorNode(connector);
+
+ connectorNode.start(scheduler);
+ assertTrue(connector.awaitStartEntered(5, TimeUnit.SECONDS));
+
+ final Future<Void> stopFuture = connectorNode.stop(scheduler);
+ assertEquals(ConnectorState.STOPPING, connectorNode.getCurrentState());
+
+ // The desired state returns to RUNNING before the deferred stop has
been carried out, so the Connector must be
+ // stopped and then started again rather than being left in STOPPING.
+ final Future<Void> restartFuture = connectorNode.start(scheduler);
+ assertEquals(ConnectorState.RUNNING, connectorNode.getDesiredState());
+
+ connector.releaseStart();
+
+ stopFuture.get(5, TimeUnit.SECONDS);
+ restartFuture.get(5, TimeUnit.SECONDS);
+
+ assertEquals(ConnectorState.RUNNING, connectorNode.getCurrentState());
+ assertEquals(ConnectorState.RUNNING, connectorNode.getDesiredState());
+ }
+
@Test
public void testCannotDeleteWhenStarting() throws Exception {
// Use a slow-starting connector to test deletion during STARTING state
@@ -1287,6 +1352,11 @@ public class TestStandardConnectorNode {
return createConnectorNode(sleepingConnector);
}
+ private StartBlockingConnector createStartBlockingConnector() {
+ startBlockingConnector = new StartBlockingConnector();
+ return startBlockingConnector;
+ }
+
private StandardConnectorNode createConnectorNode(final Connector
connector) throws FlowUpdateException {
final SecretsManager defaultSecretsManager =
mock(SecretsManager.class);
when(defaultSecretsManager.getAllSecrets()).thenReturn(List.of());
@@ -1966,6 +2036,95 @@ public class TestStandardConnectorNode {
}
}
+ /**
+ * Test connector whose start blocks until it is explicitly released, and
which records whether its stop was
+ * invoked while a start was still in progress. Used to exercise the
interleaving of a stop request with an
+ * in-flight start.
+ */
+ private static class StartBlockingConnector extends AbstractConnector {
+ private static final long MAX_START_BLOCK_SECONDS = 30L;
+
+ private final CountDownLatch startEnteredLatch = new CountDownLatch(1);
+ private final CountDownLatch startReleaseLatch = new CountDownLatch(1);
+ private final CountDownLatch stopEnteredLatch = new CountDownLatch(1);
+ private volatile boolean starting = false;
+ private volatile boolean stoppedWhileStarting = false;
+
+ @Override
+ public VersionedExternalFlow getInitialFlow() {
+ return null;
+ }
+
+ @Override
+ public VersionedExternalFlow getActiveFlow(final FlowContext
activeFlowContext) {
+ return getInitialFlow();
+ }
+
+ @Override
+ public void prepareForUpdate(final FlowContext workingContext, final
FlowContext activeContext) {
+ }
+
+ @Override
+ public List<ConfigurationStep> getConfigurationSteps() {
+ return List.of();
+ }
+
+ @Override
+ public void applyUpdate(final FlowContext workingContext, final
FlowContext activeContext) {
+ }
+
+ @Override
+ protected void onStepConfigured(final String stepName, final
FlowContext workingContext) {
+ }
+
+ @Override
+ public List<ConfigVerificationResult> verifyConfigurationStep(final
String stepName, final Map<String, String> overrides, final FlowContext
flowContext) {
+ return List.of();
+ }
+
+ @Override
+ public void start(final FlowContext activeContext) throws
FlowUpdateException {
+ starting = true;
+ startEnteredLatch.countDown();
+
+ // The wait is bounded so that a test which fails before releasing
the start cannot leave a scheduler
+ // thread blocked indefinitely, which would hang shutdown of the
scheduler during teardown.
+ try {
+ startReleaseLatch.await(MAX_START_BLOCK_SECONDS,
TimeUnit.SECONDS);
+ } catch (final InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new FlowUpdateException(e);
+ } finally {
+ starting = false;
+ }
+ }
+
+ @Override
+ public void stop(final FlowContext activeContext) {
+ if (starting) {
+ stoppedWhileStarting = true;
+ }
+
+ stopEnteredLatch.countDown();
+ }
+
+ public boolean awaitStartEntered(final long timeout, final TimeUnit
unit) throws InterruptedException {
+ return startEnteredLatch.await(timeout, unit);
+ }
+
+ public boolean awaitStopEntered(final long timeout, final TimeUnit
unit) throws InterruptedException {
+ return stopEnteredLatch.await(timeout, unit);
+ }
+
+ public void releaseStart() {
+ startReleaseLatch.countDown();
+ }
+
+ public boolean wasStoppedWhileStarting() {
+ return stoppedWhileStarting;
+ }
+ }
+
/**
* Test connector that allows control over when drainFlowFiles completes
via a CompletableFuture
*/