Repository navigation
[runtime] Fix lifecycle and metric bookkeeping gaps in parallel action execution #1224
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
ca7caa9
307fc47
790abd0
4089c0c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1584,6 +1584,10 @@ private ActionTask pollNextActionTask() throws Exception { | |
| } | ||
|
|
||
| private void tryResumeProcessActionTasks() throws Exception { | ||
| // Keys with at least one lifecycle-visible restored task, meaning a task other than the | ||
| // synthetic noop input action. The parallel restore path emits the matching record start | ||
| // for them, because completion emits one record finished per key. | ||
| Set<Object> keysWithVisibleTask = new LinkedHashSet<>(); | ||
| stateManager.forEachActionTaskKey( | ||
| getKeyedStateBackend(), | ||
| (key, state) -> { | ||
|
|
@@ -1601,10 +1605,21 @@ private void tryResumeProcessActionTasks() throws Exception { | |
| // its root caller still awaits it (the caller resumes only after the child | ||
| // quiesces), so the root is always retained here to drive the replay. | ||
| state.update(rootTasks); | ||
| boolean lifecycleVisible = false; | ||
| for (ActionTask actionTask : rootTasks) { | ||
| // The synthetic noop input action stays out of action metrics, as it does | ||
| // on the dequeue path: counting it here would leave the pending-task gauge | ||
| // nonzero once the restored task completes. | ||
| if (isInternalNoopInputAction(actionTask.action)) { | ||
| continue; | ||
| } | ||
| builtInMetrics.restoreActionTask( | ||
| actionTask.getTraceContext(), | ||
| actionTask.hasExecutionStartedEventEmitted()); | ||
| lifecycleVisible = true; | ||
| } | ||
| if (lifecycleVisible) { | ||
| keysWithVisibleTask.add(key); | ||
| } | ||
| }); | ||
|
|
||
|
|
@@ -1636,6 +1651,13 @@ private void tryResumeProcessActionTasks() throws Exception { | |
| for (int i = 0; i < queued; i++) { | ||
| executionCoordinator.addTask(key); | ||
| } | ||
| // Mirror the fresh-record path, which emits one record start per key: the | ||
| // worker-completion path emits one record finished per key, so the resumed | ||
| // round needs the matching start. A key whose restored tasks are all synthetic | ||
| // noop stays silent, because its completion is silent too. | ||
| if (keysWithVisibleTask.contains(key)) { | ||
| notifyRecordStart(key); | ||
|
Comment on lines
+1658
to
+1659
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With Could the start and finish paths use the same record-level criterion, with a regression test for this mixed queue? |
||
| } | ||
| } else { | ||
| // The normal engine has no workers; the kick mail drives | ||
| // processActionTaskForKey. | ||
|
|
@@ -1723,6 +1745,7 @@ public void execute() { | |
| setCurrentKey(key); | ||
| ActionTask task = pollNextActionTask(); | ||
| checkState(task != null, "Action task queue was empty."); | ||
| this.actionTask = task; | ||
|
|
||
| contextManager.createAndSetRunnerContext( | ||
| task, | ||
|
|
@@ -1774,7 +1797,6 @@ public void execute() { | |
| context.setActionTask(task); | ||
| context.setPriority(recordIndex, taskIndex); | ||
|
|
||
| this.actionTask = task; | ||
| this.sequenceNumber = seq; | ||
| this.replayCompletedAction = replay; | ||
|
|
||
|
|
@@ -1848,6 +1870,14 @@ public boolean isDone() { | |
| @Override | ||
| public void commit() throws Exception { | ||
| if (failure != null) { | ||
| // 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. | ||
| // actionTask is bound right after the poll, so it is non-null on a preparation | ||
| // failure too; guard anyway and always rethrow the original failure. | ||
| if (actionTask != null) { | ||
| builtInMetrics.markInputRunFailed(actionTask.getTraceContext().getInputRunId()); | ||
| } | ||
| throw new ActionTaskExecutionException("Failed to execute action task", failure); | ||
| } | ||
| checkState(result != null, "Action task completion result must not be null."); | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -18,7 +18,9 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| package org.apache.flink.agents.runtime.operator; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.apache.flink.agents.api.Event; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.apache.flink.agents.api.EventType; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.apache.flink.agents.api.InputEvent; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.apache.flink.agents.api.agents.Agent; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.apache.flink.agents.api.agents.AgentExecutionOptions; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.apache.flink.agents.api.context.DurableCallable; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.apache.flink.agents.api.context.RunnerContext; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -27,6 +29,7 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.apache.flink.agents.plan.JavaFunction; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.apache.flink.agents.plan.actions.Action; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.apache.flink.agents.runtime.async.ContinuationActionExecutor; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.apache.flink.agents.runtime.lifecycle.TaskLifecycleListener; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.apache.flink.agents.runtime.metrics.FlinkAgentsMetricGroupImpl; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.apache.flink.api.common.typeinfo.TypeInformation; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.apache.flink.api.java.functions.KeySelector; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -44,6 +47,7 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.concurrent.TimeUnit; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import static org.assertj.core.api.Assertions.assertThat; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import static org.assertj.core.api.Assertions.assertThatThrownBy; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import static org.assertj.core.api.Assumptions.assumeThat; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** Component-level tests for metrics bookkeeping on the JDK<21 parallel execution path. */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -232,4 +236,78 @@ private static AgentPlan getAgentPlan() throws Exception { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return new AgentPlan(actions, new LinkedHashMap<>(), parallelConfig()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Test | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Timeout(20) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| void failedParallelActionMarksInputRunFailed() 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); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assertThatThrownBy( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| () -> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| testHarness.processElement(new StreamRecord<>(0L)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| operator.waitInFlightEventsFinished(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .hasStackTraceContaining("parallel action boom"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assertThat(metricGroup(operator).getCounter("numOfInputRunsFailed").getCount()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .as("a failure propagated through commit() must close the input run") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .isEqualTo(1L); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Regression test: fails on 1da2fa6 with the NPE, passes with the
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** Agent whose only action always fails, exercising the parallel failure path. */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public static class FailingAgent extends Agent { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @org.apache.flink.agents.api.annotation.Action(EventType.InputEvent) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public static void handleInput(Event event, RunnerContext context) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new IllegalStateException("parallel action boom"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Test | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Timeout(20) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| void preparationFailureKeepsCauseAndMarksInputRunFailed() throws Exception { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assumeThat(ContinuationActionExecutor.isContinuationSupported()).isFalse(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try (KeyedOneInputStreamOperatorTestHarness<Long, Long, Object> testHarness = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| newHarness(new AgentPlan(new PlainAgent(), parallelConfig()))) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| testHarness.open(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ActionExecutionOperator<Long, Object> operator = operator(testHarness); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // A preparation-time listener failure must surface as the original exception, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // not as a NullPointerException from commit() dereferencing a null actionTask, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // and must still close the input run so the failure is counted. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| operator.addTaskLifecycleListener( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| new 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()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .as( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "a preparation failure propagated through commit() must close the input run") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .isEqualTo(1L); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** Agent whose action does nothing, so the only failure comes from the listener. */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public static class PlainAgent extends Agent { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @org.apache.flink.agents.api.annotation.Action(EventType.InputEvent) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public static void handleInput(Event event, RunnerContext context) {} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 leavesnumOfActiveInputRunsat 1.Could we retain input-run identification here while skipping only the action-level metrics, and cover this with a restore regression test?