Skip to content

Fix pipe task start failures: report, roll back and recover - #19119

Merged
jt2594838 merged 5 commits into
apache:masterfrom
luoluoyuyu:fix/pipe-subtask-start-recovery
Nov 4, 2026
Merged

jt2594838 merged 5 commits into
apache:masterfrom
luoluoyuyu:fix/pipe-subtask-start-recovery

Conversation

@luoluoyuyu

@luoluoyuyu luoluoyuyu commented Nov 4, 2026 •

Copy link
Copy Markdown
Member

Motivation

A pipe sink subtask can fail to start and still be reported as started, and it may never recover by itself:

  1. PipeSubtaskExecutor.start() calls allowSubmittingSelf() before submitSelf(). When submitSelf() throws - for example because the executor has been shut down - the "submitting self" flag has already been set. Every later start() then takes the "already running" branch, logs only at debug level and returns normally, so PipeSinkSubtaskLifeCycle.start() increments runningTaskCount and the config node sees a successful start while the sink never transfers a single event.

  2. PipeAbstractSinkSubtask.submitSelf() sets isSubmitted before scheduling the delayed submission, and it does not handle a rejected submission at all. When the submission or the delayed scheduling is rejected, nothing is dispatched while the sign stays set, so every following submission is skipped; on the round-completion path the rejection is thrown from the callback thread, where it is swallowed. In both cases the subtask stops transferring events without leaving any trace in the log.

  3. IoTConsensusV2SubtaskExecutor is a node-wide singleton shared by all consensus pipes. PipeSubtaskExecutorManager.getConsensusExecutor() handed it out without checking whether it had been shut down, so after a shutdown every consensus pipe registered later was bound to a dead pool and could never start its sink.

  4. PipeTaskAgent.startPipeTask() let the exception of PipeTask.start() escape without rolling back the task it had just created. The task meta was already registered, so the following meta sync found an existing task meta with an unchanged leader and never called start() again: the pipe stayed RUNNING in the metadata while running nothing until the node restarted.

  5. isSubmitted must not be cleared on an ordinary stop: stop() only flips the stop flag and does not wait for the worker, so a quick stop()/start() could submit the same sink subtask while its previous call is still running, and a pending delayed submission could pass the stop check after the restart and submit again. It may only be cleared when the previous call has completed or the submission was definitely rejected.

These were observed together on a 2.0.11 cluster: the shared consensus executor was shut down at 20:14, 384 consensus pipes were re-created at 20:16, every sink submission was rejected, and the pipes stayed "running" without transferring any event until the node was restarted.

Modification

  • PipeSubtaskExecutor.start(): fail fast with RejectedExecutionException when the executor has been shut down, before touching the subtask state, so a dead executor can never be reported as a successful start. When submitSelf() still fails, roll back the "submitting self" state, log at error level and rethrow.
  • PipeAbstractSinkSubtask: keep isSubmitted as the single-flight flag and do not clear it in onDisallowSubmittingSelf(). It is only cleared when the previous call has completed (onSuccess/onFailure) or when the submission was definitely rejected, i.e. no worker was queued. A rejected submission is reported as a warning instead of being swallowed.
  • PipeSubtaskExecutorManager.getConsensusExecutor(): recreate the executor and install it back into IoTV2GlobalComponentContainer when the current one has been shut down.
  • PipeTaskAgent.startPipeTask(): drop the failed task together with its task meta, so that the next meta sync creates and starts it again, and log the failure instead of carrying it away.

Behavior notes

  • start() on a shut-down executor now throws instead of logging a false success. PipeSinkSubtaskLifeCycle.start() only increments runningTaskCount after a successful executor.start(), and PipeTaskAgent.startPipeTask() rolls the task back on failure.
  • A sink subtask that is stopped and restarted while its previous call is still running is not submitted a second time: the in-flight call finishes and re-submits itself after the restart.

Tests

  • PipeSinkSubtaskExecutorTest#testStartWhileThePreviousCallIsStillRunning: stopping and starting a subtask while its first call is blocked does not start a second concurrent call.
  • PipeSinkSubtaskExecutorTest#testStartAfterShutdownWithPendingDelayedSubmission: with a pending delayed submission, start() after shutdown still fails.
  • PipeSinkSubtaskExecutorTest#testStartAfterShutdown: starting on a shut-down executor throws and does not leave the subtask in the submitting state.
  • PipeSinkSubtaskExecutorTest#testSubmitSelfAfterShutdown and #testDelayedSubmitSelfAfterShutdown: a rejected submission is reported and clears the submitted flag, so a following submission is still attempted.
  • PipeSubtaskExecutorTest#testStartWithRejectedSubmission: a rejected submission rolls back the submitting state and still fails.
  • PipeSubtaskExecutorManagerTest: the consensus executor is a singleton while it is alive and is recreated after it has been shut down; the shared executor is recreated after each test so other tests are not affected.
  • PipeDataNodeTaskAgentTest#testFailedTaskStartIsRolledBackForRetry: a failed task start drops the task and its task meta.

Verification: mvn spotless:check, mvn test-compile -DskipTests and mvn test-compile -DskipTests -P with-zh-locale pass, and the affected test classes (31 tests) pass. The new tests fail against the pre-fix code: the start/rejection tests fail against the pre-fix node-commons classes, and the single-flight test fails when the onDisallowSubmittingSelf() override is restored. The datanode module could not be compiled locally against current master because the Thrift compiler is not available in this environment, so its compilation is left to CI.

…ission

PipeSubtaskExecutor.start() marked the subtask as "submitting self" before
calling submitSelf(). When the submission was rejected, e.g. because the
worker pool had been shut down, the flag stayed set, so every later start()
took the "already running" branch, logged nothing and reported success while
the subtask never ran until the node restarted. The flag is now rolled back
on failure and the rejection is logged and rethrown, so the failure reaches
the pipe task agent and the config node instead of being masked as success.

PipeAbstractSinkSubtask also clears its "isSubmitted" sign when submitting is
disallowed, so a submission that was never dispatched to the worker pool can
be submitted again.

The IoTConsensusV2 subtask executor is shared by all the consensus pipes of a
node. Once it had been shut down, getConsensusExecutor() kept returning the
dead pool, so every consensus pipe registered afterwards could never start its
sink subtask. A shut-down executor is now recreated and installed back into
IoTV2GlobalComponentContainer.
startPipeTask() let an exception of PipeTask.start() escape without rolling
back the task it had just created. The task meta was already registered, so
the following meta sync found an existing task meta with the same leader and
never called start() again: the pipe stayed "RUNNING" in the metadata while
running nothing until the node restarted.

The failed task is now dropped together with its task meta, so that the next
meta sync recreates and starts the task again, which gives the task another
chance to start once the underlying problem is gone. The failure itself is
logged at error level instead of being carried away by the meta sync loop.
…pping silently

A sink subtask submits itself again after the end of each round. If the
submission is rejected, e.g. because the worker pool is being shut down while
the subtask is finishing a round, the rejection escaped from the callback
thread where it was swallowed, and, for a delayed submission, the "submitted"
sign had already been set before scheduling the delayed task, so it was never
cleared. In both cases the subtask was never submitted again and stopped
transferring events without leaving any trace in the log.

The sign is now cleared and the rejection is reported whenever the submission
or the delayed scheduling fails, so the failure is visible and a following
start of the subtask can submit it again.
// later allowSubmittingSelf() + submitSelf() can really submit the subtask again, even if the
// previous submission was never dispatched to the worker pool, e.g., because the submission was
// rejected or scheduled on an executor that has been shut down.
isSubmitted = false;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Preserve the submitted state while a sink is in flight. disallowSubmittingSelf() is also used by an ordinary stop, and PipeSubtaskExecutor.stop() only flips the stop flag without waiting for the worker. Clearing isSubmitted here lets a quick stop/start submit the same sink subtask while its previous worker is still running. A pending delayed lambda can also pass the shouldStopSubmittingSelf check after restart and submit in addition to the new run. That breaks the non-parallel invariant and can concurrently consume or send the same events. Please clear this flag only when the previous submission was definitely rejected, or otherwise prevent old work from resuming after restart.

PipeAbstractSinkSubtask#onDisallowSubmittingSelf cleared isSubmitted while
a submitted call could still be running. A following start() then submitted
the same subtask again, so two workers could execute one sink subtask
concurrently, which PipeSubtask does not support.

Keep isSubmitted as the single-flight flag: a call in flight is allowed to
finish and re-submit itself once start() has cleared the stop flag. Instead,
make PipeSubtaskExecutor#start fail fast when the executor has been shut
down, before touching any subtask state. A shut-down executor can never run
the subtask, so start() must not report a false success. This also covers a
delayed submission that is still pending when the executor is shut down.

Add regression tests for stopping and restarting a subtask while its first
call is blocked, and for starting a subtask with a pending delayed
submission after shutdown. Recreate the shared consensus executor after the
manager tests so they do not leave it shut down for other tests.
Replace the verbose inline comments in the pipe task start fix with concise
ones, and state the scenario and expected behavior in a short Javadoc above
each new test.
@luoluoyuyu luoluoyuyu changed the title Fix pipe subtask start reporting success after a rejected submission and never recovering Fix pipe task start failures: report, roll back and recover Nov 4, 2026
@jt2594838
jt2594838 merged commit dfce814 into apache:master Nov 4, 2026
37 of 39 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants