Repository navigation
[runtime] Fix lifecycle and metric bookkeeping gaps in parallel action execution - #1224
CodeTrainerMan wants to merge 4 commits into
Conversation
sangkyoonnam
left a comment
There was a problem hiding this comment.
The new markInputRunFailed call in Work.commit() turns a preparation failure into an NPE that hides the original exception.
Work.actionTask is assigned only after preparation: runner context creation, onActionPrepared listeners, durable state setup and the continuation context lookup. When any of those throws, execute() records the failure with actionTask still null, and commit() now dereferences it. On JDK 17, with a listener whose onActionPrepared throws, the test in the inline suggestion fails on 1da2fa6 with Cannot invoke "org.apache.flink.agents.runtime.operator.ActionTask.getTraceContext()" because "this.actionTask" is null. On bd527d7 the same failure surfaces as ActionTaskExecutionException caused by the listener's exception.
The fix has two parts. One is outside the diff, so it can't be a suggestion: in Work.execute(), record the task as soon as it is polled.
checkState(task != null, "Action task queue was empty.");
this.actionTask = task;The later assignment then becomes redundant. The commit() guard and a regression test are suggestions inline. With all three, the test passes and numOfInputRunsFailed is 1. That matches the serial path for preparation exceptions: it reads the input run id right after polling and marks the run failed if preparation throws an Exception. ActionExecutionOperatorTest, ActionExecutionOperatorParallelMetricsTest, TaskLifecycleListenerNotificationTest and ParallelExecutionCoordinatorTest pass with the change (98 tests), and tools/lint.sh -c is clean on JDK 11.
For contract 1 (restored record start/finish pairing), the harness can retain a queued task on the parallel engine. ActionExecutionOperatorTest.restoreRebuildsInFlightBudgetFromProcessingKeysAndPendingEvents snapshots with key 2 queued behind the busy worker, because pre-barrier draining pauses new pulls and waits for already-pulled tasks to commit. Restoring that snapshot through a factory that registers a recording listener before open(), like ListenerInjectingOperatorFactory in TaskLifecycleListenerNotificationTest, looks like it would cover contract 1. I haven't tried it. Would you rather add that here or in a follow-up?
| // This engine propagates an action failure through commit() rather than through | ||
| // processActionTaskForKey(), so the input run is closed here: the failure counter, | ||
| // the active-run gauge and the terminal latency samples all depend on it. | ||
| builtInMetrics.markInputRunFailed(actionTask.getTraceContext().getInputRunId()); |
There was a problem hiding this comment.
With the execute() change from the review body, this guard is still needed for failures before actionTask is assigned, including pollNextActionTask() throwing or the empty-queue check failing.
| builtInMetrics.markInputRunFailed(actionTask.getTraceContext().getInputRunId()); | |
| if (actionTask != null) { | |
| builtInMetrics.markInputRunFailed(actionTask.getTraceContext().getInputRunId()); | |
| } |
| .as("a failure propagated through commit() must close the input run") | ||
| .isEqualTo(1L); | ||
| } | ||
| } |
There was a problem hiding this comment.
Regression test: fails on 1da2fa6 with the NPE, passes with the execute() change and the guard above. The listener type is fully qualified so this applies without an import change outside the diff.
| } | |
| } | |
| @Test | |
| @Timeout(20) | |
| void preparationFailureKeepsCauseAndMarksInputRunFailed() throws Exception { | |
| assumeThat(ContinuationActionExecutor.isContinuationSupported()).isFalse(); | |
| try (KeyedOneInputStreamOperatorTestHarness<Long, Long, Object> testHarness = | |
| newHarness(new AgentPlan(new FailingAgent(), parallelConfig()))) { | |
| testHarness.open(); | |
| ActionExecutionOperator<Long, Object> operator = operator(testHarness); | |
| operator.addTaskLifecycleListener( | |
| new org.apache.flink.agents.runtime.lifecycle.TaskLifecycleListener() { | |
| @Override | |
| public void onActionPrepared(ActionTask task) { | |
| throw new IllegalStateException("prepare boom"); | |
| } | |
| }); | |
| assertThatThrownBy( | |
| () -> { | |
| testHarness.processElement(new StreamRecord<>(0L)); | |
| operator.waitInFlightEventsFinished(); | |
| }) | |
| .hasStackTraceContaining("prepare boom"); | |
| assertThat(metricGroup(operator).getCounter("numOfInputRunsFailed").getCount()) | |
| .isEqualTo(1L); | |
| } | |
| } |
|
One more restore interaction, related to #1238. In the restore path, At 1da2fa6 the new noop Could the noop path still identify the run and only skip the action-level metrics, for example through a run-only method on |
…utionOperator Commit() dereferenced actionTask without a null guard, so a preparation-time failure (e.g. a TaskLifecycleListener throwing in onActionPrepared) surfaced as a NullPointerException that masked the original exception. Bind actionTask right after the poll and guard the markInputRunFailed call in commit(), so the original failure is always rethrown. Adds a regression test covering the preparation-failure path. Covers review feedback on apache#1224.
|
Thanks @sangkyoonnam — both suggestions are applied and the regression test now matches your sketch.
Regression test: The earlier CI "Java format check failed" was only import ordering in the new test (a non-static import sat below the static imports); fixed via |
…tion On the parallel action execution engine used when continuations are unsupported (JDK < 21): - Document that lifecycle callbacks run on the thread driving the reported execution instead of always on the mailbox thread, on both the task and the component execution listener contracts. - Emit onRecordStart when the restore path requeues unfinished tasks, once per key, so the resumed round pairs with the record finished emitted by the worker-completion path. - Skip the synthetic noop input action when restoring action metrics, matching the dequeue path, so the pending action task gauge returns to zero. - Mark the input run failed before propagating an action failure from commit(), so failures are counted and the active run gauge and the latency samples are released. Addresses items 1, 2, 4 and 5 of apache#1218. Item 3, rebinding subagent tasks on restore, is not covered.
…utionOperator Commit() dereferenced actionTask without a null guard, so a preparation-time failure (e.g. a TaskLifecycleListener throwing in onActionPrepared) surfaced as a NullPointerException that masked the original exception. Bind actionTask right after the poll and guard the markInputRunFailed call in commit(), so the original failure is always rethrown. Adds a regression test covering the preparation-failure path. Covers review feedback on apache#1224.
Reorder the newly added imports to satisfy google-java-format (AOSP) import ordering, fixing the CI format check.
…ed failed Rename the preparation-failure test to preparationFailureKeepsCauseAndMarksInputRunFailed and assert numOfInputRunsFailed is incremented, matching the review suggestion. The fix binds actionTask right after the poll, so the guard still marks the run on a preparation failure.
593f3bc to
4089c0c
Compare
|
Thanks, the preparation-failure fix looks good to me. At 4089c0c The restore case from my later comment is still open. Take the parallel engine with one owned processing key, no pending inputs and a single retained noop task. The noop Could |
wenjin272
left a comment
There was a problem hiding this comment.
Thanks @CodeTrainerMan for working on this, and sorry for the late review. The preparation-failure fix looks good. I left two inline comments on the remaining restore cases; could you take a look?
| if (keysWithVisibleTask.contains(key)) { | ||
| notifyRecordStart(key); |
There was a problem hiding this comment.
With AGENT_RUN_BEGIN_EVENT enabled, an Agent that handles AgentRunBeginEvent but has no InputEvent handler can restore a queue of [beginHandler, internalNoop]. This condition emits onRecordStart, but maybeFinishCurrentInput() suppresses onRecordFinished because the last task is the noop. I reproduced this through checkpoint/restore on JDK 17: the listener receives only start:1.
Could the start and finish paths use the same record-level criterion, with a regression test for this mixed queue?
| if (isInternalNoopInputAction(actionTask.action)) { | ||
| continue; |
There was a problem hiding this comment.
This also skips the input-run identification performed by restoreActionTask(), as @sangkyoonnam noted. The restored noop carries a non-null run ID, but completion cannot match it to the anonymous restored slot. In an operator-level reproduction, restoring a noop and then completing another fresh input leaves numOfActiveInputRuns at 1.
Could we retain input-run identification here while skipping only the action-level metrics, and cover this with a restore regression test?
Linked issue: #1218
Purpose of change
User-visible outcome
On the JDK < 21 parallel action execution engine (no coroutines):
onRecordStart/onRecordFinishedbracket after a failover, instead of aonRecordFinishedwith nomatching start.
numOfPendingActionTaskspermanently nonzero after the task completes.numOfInputRunsFailedis incremented, theactive-run gauge is decremented, and terminal samples for
inputRunLatencyMsandinputRunProcessingLatencyMsare recorded.engine does not honor.
Runtime flow
On restore,
tryResumeProcessActionTaskswalks the keyed action-task state, keeps only roottasks, restores their metrics and, per owned key, requeues one coordinator node per queued
task. The fix adds a record-start notification in that per-key branch, so the bracket opened
here is closed by
maybeFinishCurrentInput, which emits oneonRecordFinishedper key whenthe last in-flight task of the key commits. The same loop now skips the synthetic no-op input
action when restoring metrics, matching what the dequeue path already does.
On the failure path, the parallel engine runs an action to completion off the mailbox thread
and propagates its failure from
Work.commit(). That is the only place the failure isobserved on this engine, so the input run is closed there.
Key decisions
onRecordStartonce per key, not once per requeued task. Completion emits oneonRecordFinishedper key, so a per-task start would open more brackets than are closed.whose restored tasks are all synthetic no-op completes silently, so starting it would open
a bracket that is never closed.
isInternalNoopInputActionpredicate for the restore path rather thanintroducing a second notion of "lifecycle-visible", so the dequeue and restore paths cannot
disagree.
commit()rather than at the call site, becausecommit()is where this engine observes the failure;markInputRunFailedis idempotent,so it cannot double count a run that was already closed.
Behavioral Semantics
Interaction decisions
Failure path:
numOfInputRunsFailedActionTaskExecutionException, unchangedBehavioral contracts
onRecordStartfora resumed key before the
onRecordFinishedthat completion emits for that key.finished.
completes,
numOfPendingActionTasksis zero.numOfInputRunsFailedincreases by one for that input run,and the active-input-run gauge is decremented once.
TaskLifecycleListenerandComponentExecutionListenerstates that the callback thread is not fixed, so a listenerthat keeps state must synchronize it itself.
Failure behavior
rethrown as
ActionTaskExecutionException; nothing is swallowed or retried.BuiltInInputRunMetrics.finish, which removes the run id before recording, so no gauge isdecremented twice and no latency sample is recorded twice.
the serial path and is still logged and swallowed for component execution reporting.
Tests
Contracts to tests
ActionExecutionOperatorParallelMetricsTest.failedParallelActionMarksInputRunFailedCoverage by risk
The change touches restore-time bookkeeping and the failure path. Contract 4 is pinned by a
new component test that runs only when continuations are unsupported, i.e. exactly the engine
the change targets. The existing parallel and lifecycle suites
(
ActionExecutionOperatorTest,ParallelExecutionCoordinatorTest,ParallelExecutionLockTest,ParallelExecutionTaskQueueTest,TaskLifecycleListenerNotificationTest,ActionExecutionOperatorCapacityIntegrationTest,ActionExecutionOperatorDrainDeadlockIntegrationTest) cover the surrounding behavior and wererun with the change.
Not verified
snapshot on this engine: the parallel engine drains in-flight tasks before checkpointing,
so a snapshot taken while a worker is blocked waits for that worker, and a snapshot taken
after it is released leaves no queued task behind. The serial restore path is covered by
TaskLifecycleListenerNotificationTest.resumedInFlightRecordReEmitsRecordStart; theparallel restore path is not.
restore whose queued tasks mix synthetic no-op and ordinary tasks on one key.
Implementation invariants and supporting evidence
markInputRunFailedis idempotent:BuiltInInputRunMetrics.finishremoves the run id fromactiveInputRunIdsandactiveRunTimingsbefore recording, and returns early when thetiming is absent, so a repeated call neither decrements the gauge twice nor records a
second sample.
Work.commit()is invoked only fromParallelExecutionCoordinator, so the serial catchblock that already calls
markInputRunFailedcannot run for the same failure.onRecordFinishedby the completion path and frommarkActionTaskDequeuedby the dequeue path; the restore path now matches both.emits no start, which matches it never emitting a finish.
API
Compatibility impact
No public API changes.
TaskLifecycleListenerandComponentExecutionListenerkeep theirsignatures and default methods; only their contract documentation changes, and it now
describes behavior the parallel engine already had. What does change for an existing caller
is observable metrics: failed input runs on the parallel engine are now counted and released,
and the pending-task gauge no longer retains restored synthetic no-op tasks. Callers that
relied on the previous counts observe different values; listeners that assumed mailbox-thread
affinity were already incorrect on this engine.
Documentation
doc-neededdoc-not-neededdoc-includedWas this patch authored or co-authored using generative AI tooling?