This is an automated email from the ASF dual-hosted git repository.
SbloodyS pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/dolphinscheduler.git
The following commit(s) were added to refs/heads/dev by this push:
new 03da5340ba [Fix-18707][Master] Keep task success retryable after
persistence failure (#18708)
03da5340ba is described below
commit 03da5340ba29e55528e86efc1c489fdf53542737
Author: ThreeFire1 <[email protected]>
AuthorDate: Fri Oct 16 15:23:12 2026 +0800
[Fix-18707][Master] Keep task success retryable after persistence failure
(#18708)
---
.../task/statemachine/AbstractTaskStateAction.java | 76 ++-
.../statemachine/TaskSubmittedStateAction.java | 7 +-
.../TaskLifecyclePersistenceRecoveryTest.java | 502 ++++++++++++++++++
.../TaskSuccessPersistenceRecoveryTest.java | 564 +++++++++++++++++++++
.../TaskLifecyclePersistenceRecoveryTestCase.java | 134 +++++
.../TaskSuccessPersistenceRecoveryTestCase.java | 93 ++++
6 files changed, 1344 insertions(+), 32 deletions(-)
diff --git
a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/task/statemachine/AbstractTaskStateAction.java
b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/task/statemachine/AbstractTaskStateAction.java
index d053676e88..b62fe66c74 100644
---
a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/task/statemachine/AbstractTaskStateAction.java
+++
b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/task/statemachine/AbstractTaskStateAction.java
@@ -49,9 +49,11 @@ import
org.apache.dolphinscheduler.server.master.engine.workflow.lifecycle.event
import org.apache.commons.lang3.StringUtils;
import java.util.List;
+import java.util.function.Consumer;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import com.google.common.collect.Lists;
@@ -124,9 +126,10 @@ public abstract class AbstractTaskStateAction implements
ITaskStateAction {
private void persistentTaskInstanceFatalEventToDB(final ITaskExecution
taskExecution,
final
TaskFatalLifecycleEvent taskFatalEvent) {
final TaskInstance taskInstance = taskExecution.getTaskInstance();
- taskInstance.setState(TaskExecutionStatus.FAILURE);
- taskInstance.setEndTime(taskFatalEvent.getEndTime());
- taskInstanceDao.updateById(taskInstance);
+ persistTaskInstanceUpdate(taskInstance, instance -> {
+ instance.setState(TaskExecutionStatus.FAILURE);
+ instance.setEndTime(taskFatalEvent.getEndTime());
+ });
}
@Override
@@ -134,9 +137,10 @@ public abstract class AbstractTaskStateAction implements
ITaskStateAction {
final ITaskExecution taskExecution,
final TaskDispatchedLifecycleEvent
taskDispatchedEvent) {
final TaskInstance taskInstance = taskExecution.getTaskInstance();
- taskInstance.setState(DISPATCH);
- taskInstance.setHost(taskDispatchedEvent.getExecutorHost());
- taskInstanceDao.updateById(taskInstance);
+ persistTaskInstanceUpdate(taskInstance, instance -> {
+ instance.setState(DISPATCH);
+ instance.setHost(taskDispatchedEvent.getExecutorHost());
+ });
}
@Override
@@ -144,19 +148,21 @@ public abstract class AbstractTaskStateAction implements
ITaskStateAction {
final ITaskExecution
taskExecution,
final
TaskRuntimeContextChangedEvent taskRuntimeContextChangedEvent) {
final TaskInstance taskInstance = taskExecution.getTaskInstance();
- if
(StringUtils.isNotEmpty(taskRuntimeContextChangedEvent.getRuntimeContext())) {
-
taskInstance.setAppLink(taskRuntimeContextChangedEvent.getRuntimeContext());
- }
- taskInstanceDao.updateById(taskInstance);
+ persistTaskInstanceUpdate(taskInstance, instance -> {
+ if
(StringUtils.isNotEmpty(taskRuntimeContextChangedEvent.getRuntimeContext())) {
+
instance.setAppLink(taskRuntimeContextChangedEvent.getRuntimeContext());
+ }
+ });
}
protected void persistentTaskInstanceStartedEventToDB(final ITaskExecution
taskExecution,
final
TaskRunningLifecycleEvent taskRunningEvent) {
final TaskInstance taskInstance = taskExecution.getTaskInstance();
- taskInstance.setState(TaskExecutionStatus.RUNNING_EXECUTION);
- taskInstance.setStartTime(taskRunningEvent.getStartTime());
- taskInstance.setLogPath(taskRunningEvent.getLogPath());
- taskInstanceDao.updateById(taskInstance);
+ persistTaskInstanceUpdate(taskInstance, instance -> {
+ instance.setState(TaskExecutionStatus.RUNNING_EXECUTION);
+ instance.setStartTime(taskRunningEvent.getStartTime());
+ instance.setLogPath(taskRunningEvent.getLogPath());
+ });
}
@Override
@@ -172,8 +178,7 @@ public abstract class AbstractTaskStateAction implements
ITaskStateAction {
private void persistentTaskInstancePausedEventToDB(final ITaskExecution
taskExecution,
final
TaskPausedLifecycleEvent taskPausedEvent) {
final TaskInstance taskInstance = taskExecution.getTaskInstance();
- taskInstance.setState(TaskExecutionStatus.PAUSE);
- taskInstanceDao.updateById(taskInstance);
+ persistTaskInstanceUpdate(taskInstance, instance ->
instance.setState(TaskExecutionStatus.PAUSE));
}
@Override
@@ -189,9 +194,10 @@ public abstract class AbstractTaskStateAction implements
ITaskStateAction {
private void persistentTaskInstanceKilledEventToDB(final ITaskExecution
taskExecution,
final
TaskKilledLifecycleEvent taskKilledEvent) {
final TaskInstance taskInstance = taskExecution.getTaskInstance();
- taskInstance.setState(TaskExecutionStatus.KILL);
- taskInstance.setEndTime(taskKilledEvent.getEndTime());
- taskInstanceDao.updateById(taskInstance);
+ persistTaskInstanceUpdate(taskInstance, instance -> {
+ instance.setState(TaskExecutionStatus.KILL);
+ instance.setEndTime(taskKilledEvent.getEndTime());
+ });
}
@@ -222,9 +228,10 @@ public abstract class AbstractTaskStateAction implements
ITaskStateAction {
private void persistentTaskInstanceFailedEventToDB(final ITaskExecution
taskExecution,
final
TaskFailedLifecycleEvent taskFailedEvent) {
final TaskInstance taskInstance = taskExecution.getTaskInstance();
- taskInstance.setState(TaskExecutionStatus.FAILURE);
- taskInstance.setEndTime(taskFailedEvent.getEndTime());
- taskInstanceDao.updateById(taskInstance);
+ persistTaskInstanceUpdate(taskInstance, instance -> {
+ instance.setState(TaskExecutionStatus.FAILURE);
+ instance.setEndTime(taskFailedEvent.getEndTime());
+ });
}
@Override
@@ -246,15 +253,32 @@ public abstract class AbstractTaskStateAction implements
ITaskStateAction {
workflowInstance.setVarPool(VarPoolUtils.serializeVarPool(finalVarPool));
}
+ /**
+ * Persist an event's fields before applying them to the shared instance,
so a failed update remains retryable.
+ * The updater must only assign the event's fields and must be safe to
apply to both instances. Compute merged
+ * values before calling this method; do not publish events or mutate
nested objects in the updater.
+ */
+ protected void persistTaskInstanceUpdate(final TaskInstance taskInstance,
+ final Consumer<TaskInstance>
updater) {
+ final TaskInstance taskInstanceToUpdate = new TaskInstance();
+ BeanUtils.copyProperties(taskInstance, taskInstanceToUpdate);
+ updater.accept(taskInstanceToUpdate);
+ taskInstanceDao.updateById(taskInstanceToUpdate);
+ updater.accept(taskInstance);
+ }
+
protected void persistentTaskInstanceSuccessEventToDB(final ITaskExecution
taskExecution,
final
TaskSuccessLifecycleEvent taskSuccessEvent) {
final TaskInstance taskInstance = taskExecution.getTaskInstance();
- taskInstance.setState(TaskExecutionStatus.SUCCESS);
- taskInstance.setEndTime(taskSuccessEvent.getEndTime());
final List<Property> finalVarPool =
VarPoolUtils.mergeVarPoolJsonString(taskInstance.getVarPool(),
JSONUtils.toJsonString(taskSuccessEvent.getVarPool()));
- taskInstance.setVarPool(VarPoolUtils.serializeVarPool(finalVarPool));
- taskInstanceDao.updateById(taskInstance);
+ final String serializedFinalVarPool =
VarPoolUtils.serializeVarPool(finalVarPool);
+
+ persistTaskInstanceUpdate(taskInstance, instance -> {
+ instance.setState(TaskExecutionStatus.SUCCESS);
+ instance.setEndTime(taskSuccessEvent.getEndTime());
+ instance.setVarPool(serializedFinalVarPool);
+ });
}
/**
diff --git
a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/task/statemachine/TaskSubmittedStateAction.java
b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/task/statemachine/TaskSubmittedStateAction.java
index 05b98f93c7..ec055cc5e9 100644
---
a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/task/statemachine/TaskSubmittedStateAction.java
+++
b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/task/statemachine/TaskSubmittedStateAction.java
@@ -19,7 +19,6 @@ package
org.apache.dolphinscheduler.server.master.engine.task.statemachine;
import org.apache.dolphinscheduler.common.utils.DateUtils;
import org.apache.dolphinscheduler.dao.entity.TaskInstance;
-import org.apache.dolphinscheduler.dao.repository.TaskInstanceDao;
import org.apache.dolphinscheduler.plugin.task.api.enums.TaskExecutionStatus;
import
org.apache.dolphinscheduler.server.master.engine.task.dispatcher.WorkerGroupDispatcherCoordinator;
import
org.apache.dolphinscheduler.server.master.engine.task.execution.ITaskExecution;
@@ -56,9 +55,6 @@ public class TaskSubmittedStateAction extends
AbstractTaskStateAction {
@Autowired
private WorkerGroupDispatcherCoordinator workerGroupDispatcherCoordinator;
- @Autowired
- private TaskInstanceDao taskInstanceDao;
-
@Override
public void onStartEvent(final IWorkflowExecution workflowExecution,
final ITaskExecution taskExecution,
@@ -104,8 +100,7 @@ public class TaskSubmittedStateAction extends
AbstractTaskStateAction {
taskInstance.getFirstSubmitTime(),
taskInstance.getDelayTime() * 60L) * 1_000;
if (remainTimeMills > 0) {
- taskInstance.setState(TaskExecutionStatus.DELAY_EXECUTION);
- taskInstanceDao.updateById(taskInstance);
+ persistTaskInstanceUpdate(taskInstance, instance ->
instance.setState(TaskExecutionStatus.DELAY_EXECUTION));
log.info("Current taskInstance: {} is choose delay execution,
delay time: {}/min, remainTime: {}/ms",
taskInstance.getName(),
taskInstance.getDelayTime(),
diff --git
a/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/engine/task/statemachine/TaskLifecyclePersistenceRecoveryTest.java
b/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/engine/task/statemachine/TaskLifecyclePersistenceRecoveryTest.java
new file mode 100644
index 0000000000..2ba68ae3c9
--- /dev/null
+++
b/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/engine/task/statemachine/TaskLifecyclePersistenceRecoveryTest.java
@@ -0,0 +1,502 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.dolphinscheduler.server.master.engine.task.statemachine;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+import org.apache.dolphinscheduler.dao.entity.TaskGroupQueue;
+import org.apache.dolphinscheduler.dao.entity.TaskInstance;
+import org.apache.dolphinscheduler.dao.entity.WorkflowInstance;
+import org.apache.dolphinscheduler.dao.repository.TaskGroupQueueDao;
+import org.apache.dolphinscheduler.dao.repository.TaskInstanceDao;
+import org.apache.dolphinscheduler.plugin.task.api.enums.TaskExecutionStatus;
+import org.apache.dolphinscheduler.server.master.engine.AbstractLifecycleEvent;
+import org.apache.dolphinscheduler.server.master.engine.ILifecycleEventHandler;
+import org.apache.dolphinscheduler.server.master.engine.ILifecycleEventType;
+import org.apache.dolphinscheduler.server.master.engine.ITaskGroupCoordinator;
+import org.apache.dolphinscheduler.server.master.engine.TaskGroupCoordinator;
+import org.apache.dolphinscheduler.server.master.engine.WorkflowEventBus;
+import
org.apache.dolphinscheduler.server.master.engine.WorkflowEventBusFireWorker;
+import
org.apache.dolphinscheduler.server.master.engine.graph.IWorkflowExecutionGraph;
+import
org.apache.dolphinscheduler.server.master.engine.task.client.TaskExecutorClient;
+import
org.apache.dolphinscheduler.server.master.engine.task.dispatcher.WorkerGroupDispatcherCoordinator;
+import
org.apache.dolphinscheduler.server.master.engine.task.execution.ITaskExecution;
+import
org.apache.dolphinscheduler.server.master.engine.task.lifecycle.AbstractTaskLifecycleEvent;
+import
org.apache.dolphinscheduler.server.master.engine.task.lifecycle.event.TaskDispatchLifecycleEvent;
+import
org.apache.dolphinscheduler.server.master.engine.task.lifecycle.event.TaskDispatchedLifecycleEvent;
+import
org.apache.dolphinscheduler.server.master.engine.task.lifecycle.event.TaskFailedLifecycleEvent;
+import
org.apache.dolphinscheduler.server.master.engine.task.lifecycle.event.TaskFatalLifecycleEvent;
+import
org.apache.dolphinscheduler.server.master.engine.task.lifecycle.event.TaskKilledLifecycleEvent;
+import
org.apache.dolphinscheduler.server.master.engine.task.lifecycle.event.TaskPausedLifecycleEvent;
+import
org.apache.dolphinscheduler.server.master.engine.task.lifecycle.event.TaskRetryLifecycleEvent;
+import
org.apache.dolphinscheduler.server.master.engine.task.lifecycle.event.TaskRunningLifecycleEvent;
+import
org.apache.dolphinscheduler.server.master.engine.task.lifecycle.event.TaskRuntimeContextChangedEvent;
+import
org.apache.dolphinscheduler.server.master.engine.task.lifecycle.handler.AbstractTaskLifecycleEventHandler;
+import
org.apache.dolphinscheduler.server.master.engine.task.lifecycle.handler.TaskDispatchLifecycleEventHandler;
+import
org.apache.dolphinscheduler.server.master.engine.task.lifecycle.handler.TaskDispatchedLifecycleEventHandler;
+import
org.apache.dolphinscheduler.server.master.engine.task.lifecycle.handler.TaskFailedLifecycleEventHandler;
+import
org.apache.dolphinscheduler.server.master.engine.task.lifecycle.handler.TaskFatalLifecycleEventHandler;
+import
org.apache.dolphinscheduler.server.master.engine.task.lifecycle.handler.TaskKilledLifecycleEventHandler;
+import
org.apache.dolphinscheduler.server.master.engine.task.lifecycle.handler.TaskPausedLifecycleEventHandler;
+import
org.apache.dolphinscheduler.server.master.engine.task.lifecycle.handler.TaskRunningLifecycleEventHandler;
+import
org.apache.dolphinscheduler.server.master.engine.task.lifecycle.handler.TaskRuntimeContextChangedLifecycleEventHandler;
+import
org.apache.dolphinscheduler.server.master.engine.workflow.execution.IWorkflowExecution;
+import
org.apache.dolphinscheduler.server.master.engine.workflow.lifecycle.WorkflowLifecycleEventType;
+import
org.apache.dolphinscheduler.server.master.runner.IWorkflowExecuteContext;
+import
org.apache.dolphinscheduler.task.executor.eventbus.ITaskExecutorLifecycleEventReporter;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+import org.springframework.beans.BeanUtils;
+import org.springframework.dao.DataAccessResourceFailureException;
+import org.springframework.test.util.ReflectionTestUtils;
+
+class TaskLifecyclePersistenceRecoveryTest {
+
+ private enum EventType {
+ DISPATCHED, RUNNING, PAUSED, KILLED, FAILED, FATAL, CONTEXT, DELAY
+ }
+
+ @ParameterizedTest
+ @EnumSource(EventType.class)
+ void shouldKeepSharedFieldsUnchangedUntilPersistenceRecovers(final
EventType type) {
+ final Fixture fixture = new Fixture(type);
+ final List<Object> original = fields(fixture.shared);
+ fixture.fail.set(true);
+
+
assertThatThrownBy(fixture.deliver::run).isInstanceOf(DataAccessResourceFailureException.class);
+
+ assertThat(fields(fixture.shared)).isEqualTo(original);
+ assertThat(fields(fixture.persisted)).isEqualTo(original);
+ verifyNoInteractions(fixture.client, fixture.graph,
fixture.dispatcher);
+ verify(fixture.bus,
never()).publish(any(AbstractLifecycleEvent.class));
+
+ fixture.fail.set(false);
+ fixture.deliver.run();
+
+ assertThat(fixture.attempts).hasValue(2);
+ fixture.assertPersistedResult();
+ fixture.assertAck(type == EventType.FATAL || type == EventType.DELAY ?
0 : 1);
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = EventType.class, names = {"DISPATCHED", "PAUSED",
"KILLED"})
+ void
shouldRetryTheDatabaseUpdateInsteadOfAcknowledgingAnUnpersistedState(final
EventType type) {
+ final Fixture fixture = new Fixture(type);
+ fixture.fail.set(true);
+
assertThatThrownBy(fixture.deliver::run).isInstanceOf(DataAccessResourceFailureException.class);
+
+ fixture.fail.set(false);
+ fixture.deliver.run();
+
+ assertThat(fixture.attempts).hasValue(2);
+ fixture.assertPersistedResult();
+ fixture.assertAck(1);
+ }
+
+ @ParameterizedTest
+ @EnumSource(EventType.class)
+ void shouldNotAcknowledgeOrAdvanceWhilePersistenceKeepsFailing(final
EventType type) {
+ final Fixture fixture = new Fixture(type);
+ fixture.fail.set(true);
+
assertThatThrownBy(fixture.deliver::run).isInstanceOf(DataAccessResourceFailureException.class);
+
assertThatThrownBy(fixture.deliver::run).isInstanceOf(DataAccessResourceFailureException.class);
+ assertThat(fixture.attempts).hasValue(2);
+ verifyNoInteractions(fixture.client, fixture.graph,
fixture.dispatcher);
+ verify(fixture.bus,
never()).publish(any(AbstractLifecycleEvent.class));
+ }
+
+ @ParameterizedTest
+ @EnumSource(EventType.class)
+ void
shouldPersistNormallyWithoutReplacingTheSharedInstanceOrUnrelatedFields(final
EventType type) {
+ final Fixture fixture = new Fixture(type);
+
when(fixture.dao.updateById(any(TaskInstance.class))).thenAnswer(invocation -> {
+ final TaskInstance candidate = invocation.getArgument(0);
+ assertThat(candidate).isNotSameAs(fixture.shared);
+ BeanUtils.copyProperties(candidate, fixture.persisted);
+ // A field outside this event's update must not be overwritten by
copying the whole candidate back.
+ fixture.shared.setName("updated-name");
+ return true;
+ });
+ fixture.deliver.run();
+ fixture.assertPersistedResult();
+ assertThat(fixture.task.getTaskInstance()).isSameAs(fixture.shared);
+ assertThat(fixture.shared.getName()).isEqualTo("updated-name");
+ }
+
+ @ParameterizedTest
+ @EnumSource(EventType.class)
+ void shouldPreserveExistingBehaviorWhenUpdateReturnsFalse(final EventType
type) {
+ final Fixture fixture = new Fixture(type);
+
when(fixture.dao.updateById(any(TaskInstance.class))).thenReturn(false);
+ fixture.deliver.run();
+
assertThat(fixture.shared.getState()).isEqualTo(fixture.expectedState());
+ fixture.assertAck(type == EventType.FATAL || type == EventType.DELAY ?
0 : 1);
+ verify(fixture.dao, never()).queryById(any());
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = EventType.class, names = {"DISPATCHED", "PAUSED",
"KILLED"})
+ void
shouldAcknowledgeAgainWithoutRepeatingPersistenceAfterAckFailure(final
EventType type) {
+ final Fixture fixture = new Fixture(type);
+ doThrow(new IllegalStateException("synthetic ACK
failure")).doNothing().when(fixture.client)
+ .ackTaskExecutorLifecycleEvent(eq(fixture.task), any());
+
assertThatThrownBy(fixture.deliver::run).isInstanceOf(IllegalStateException.class);
+ fixture.assertPersistedResult();
+ fixture.deliver.run();
+ assertThat(fixture.attempts).hasValue(1);
+ fixture.assertAck(2);
+ verify(fixture.bus, times(type == EventType.DISPATCHED ? 0 :
1)).publish(any(AbstractLifecycleEvent.class));
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = EventType.class, names = {"FAILED", "FATAL"})
+ void shouldScheduleTaskRetryOnlyAfterFailureIsPersisted(final EventType
type) {
+ final Fixture fixture = new Fixture(type);
+ fixture.shared.setMaxRetryTimes(1);
+ when(fixture.task.isTaskInstanceCanRetry()).thenReturn(true);
+ fixture.fail.set(true);
+
assertThatThrownBy(fixture.deliver::run).isInstanceOf(DataAccessResourceFailureException.class);
+ verify(fixture.bus,
never()).publish(any(AbstractLifecycleEvent.class));
+ fixture.fail.set(false);
+ fixture.deliver.run();
+ verify(fixture.bus).publish(any(TaskRetryLifecycleEvent.class));
+ verifyNoInteractions(fixture.graph);
+ fixture.assertPersistedResult();
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = EventType.class, names = {"FAILED", "FATAL"})
+ void shouldKeepConditionSuccessorsEligibleAfterPersistenceRecovers(final
EventType type) {
+ final Fixture fixture = new Fixture(type);
+
when(fixture.graph.isAllSuccessorsAreConditionTask(fixture.task)).thenReturn(true);
+ fixture.fail.set(true);
+
assertThatThrownBy(fixture.deliver::run).isInstanceOf(DataAccessResourceFailureException.class);
+ fixture.fail.set(false);
+ fixture.deliver.run();
+ verify(fixture.graph,
never()).markTaskExecutionChainFailure(fixture.task);
+ verify(fixture.bus).publish(any(AbstractLifecycleEvent.class));
+ fixture.assertPersistedResult();
+ }
+
+ @Test
+ void shouldKeepExistingAppLinkWhenRuntimeContextIsEmpty() {
+ final Fixture fixture = new Fixture(EventType.CONTEXT);
+ fixture.bind(new
TaskRuntimeContextChangedLifecycleEventHandler(fixture.client),
+ new TaskRuntimeContextChangedEvent(fixture.task, ""));
+ fixture.deliver.run();
+ assertThat(fixture.shared.getAppLink()).isEqualTo("old-context");
+ assertThat(fixture.persisted.getAppLink()).isEqualTo("old-context");
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = EventType.class, names = {"PAUSED", "DISPATCHED"})
+ void shouldRecoverThroughTheRealWorkflowEventBus(final EventType type) {
+ final Fixture fixture = new Fixture(type);
+ final IWorkflowExecuteContext context =
mock(IWorkflowExecuteContext.class);
+ final WorkflowInstance workflowInstance =
fixture.workflow.getWorkflowInstance();
+ when(context.getWorkflowInstance()).thenReturn(workflowInstance);
+ when(fixture.workflow.getWorkflowExecuteContext()).thenReturn(context);
+ when(fixture.workflow.getId()).thenReturn(1);
+ final WorkflowEventBusFireWorker worker = new
WorkflowEventBusFireWorker();
+ worker.registerWorkflowEventBus(fixture.workflow);
+ worker.registerEventHandler(fixture.handler);
+ worker.registerEventHandler(new
ILifecycleEventHandler<AbstractLifecycleEvent>() {
+
+ @Override
+ public void handle(final IWorkflowExecution workflow, final
AbstractLifecycleEvent event) {
+ // Drain the downstream event; the task event's retry and
persistence are under test.
+ }
+
+ @Override
+ public ILifecycleEventType matchEventType() {
+ return
WorkflowLifecycleEventType.TOPOLOGY_LOGICAL_TRANSACTION_WITH_TASK_FINISH;
+ }
+ });
+ fixture.bus.publish(fixture.event);
+ fixture.fail.set(true);
+ worker.fireAllRegisteredEvent();
+ assertThat(fixture.attempts).hasValue(1);
+ fixture.assertAck(0);
+ assertThat(fixture.bus.isEmpty()).isFalse();
+ fixture.fail.set(false);
+ worker.fireAllRegisteredEvent();
+ assertThat(fixture.attempts).hasValue(2);
+ fixture.assertPersistedResult();
+ fixture.assertAck(1);
+ assertThat(fixture.bus.isEmpty()).isTrue();
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = EventType.class, names = {"PAUSED", "KILLED"})
+ void shouldRetryResourceReleaseBeforePersistingTheTerminalState(final
EventType type) {
+ final Fixture fixture = new Fixture(type);
+ final TaskGroupQueueDao queueDao =
fixture.useRealTaskGroupCoordinator();
+ when(queueDao.queryByTaskInstanceId(1))
+ .thenThrow(new DataAccessResourceFailureException("synthetic
task group query failure"))
+ .thenReturn(Collections.emptyList());
+
assertThatThrownBy(fixture.deliver::run).isInstanceOf(DataAccessResourceFailureException.class);
+
assertThat(fixture.shared.getState()).isEqualTo(TaskExecutionStatus.RUNNING_EXECUTION);
+ assertThat(fixture.attempts).hasValue(0);
+ fixture.assertAck(0);
+ fixture.deliver.run();
+ verify(queueDao, times(2)).queryByTaskInstanceId(1);
+ fixture.assertPersistedResult();
+ fixture.assertAck(1);
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = EventType.class, names = {"PAUSED", "KILLED"})
+ void
shouldRetryAQueueDeletionFailureBeforePersistingTheTerminalState(final
EventType type) {
+ final Fixture fixture = new Fixture(type);
+ final TaskGroupQueueDao queueDao =
fixture.useRealTaskGroupCoordinator();
+ final TaskGroupQueue queue = new TaskGroupQueue();
+ queue.setId(10);
+
when(queueDao.queryByTaskInstanceId(1)).thenReturn(Collections.singletonList(queue));
+ when(queueDao.deleteById(queue))
+ .thenThrow(new DataAccessResourceFailureException("synthetic
task group deletion failure"))
+ .thenReturn(true);
+
assertThatThrownBy(fixture.deliver::run).isInstanceOf(DataAccessResourceFailureException.class);
+
assertThat(fixture.shared.getState()).isEqualTo(TaskExecutionStatus.RUNNING_EXECUTION);
+ assertThat(fixture.attempts).hasValue(0);
+ fixture.assertAck(0);
+ fixture.deliver.run();
+ verify(queueDao, times(2)).deleteById(queue);
+ fixture.assertPersistedResult();
+ fixture.assertAck(1);
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = EventType.class, names = {"PAUSED", "KILLED"})
+ void
shouldNotReleaseTheSameQueueEntryAgainWhenTheTaskUpdateIsRetried(final
EventType type) {
+ final Fixture fixture = new Fixture(type);
+ final TaskGroupQueueDao queueDao =
fixture.useRealTaskGroupCoordinator();
+ final TaskGroupQueue queue = new TaskGroupQueue();
+ queue.setId(10);
+ final AtomicBoolean allocated = new AtomicBoolean(true);
+ when(queueDao.queryByTaskInstanceId(1)).thenAnswer(invocation ->
allocated.get()
+ ? Collections.singletonList(queue)
+ : Collections.emptyList());
+ when(queueDao.deleteById(queue)).thenAnswer(invocation ->
allocated.getAndSet(false));
+ fixture.fail.set(true);
+
assertThatThrownBy(fixture.deliver::run).isInstanceOf(DataAccessResourceFailureException.class);
+ assertThat(allocated).isFalse();
+ fixture.fail.set(false);
+ fixture.deliver.run();
+ verify(queueDao, times(2)).queryByTaskInstanceId(1);
+ verify(queueDao).deleteById(queue);
+ fixture.assertPersistedResult();
+ fixture.assertAck(1);
+ }
+
+ private static List<Object> fields(final TaskInstance instance) {
+ return Arrays.asList(instance.getState(), instance.getHost(),
instance.getStartTime(),
+ instance.getEndTime(), instance.getLogPath(),
instance.getAppLink(), instance.getVarPool());
+ }
+
+ private static final class Fixture {
+
+ private static final Date EVENT_TIME = new Date(1_000L);
+ private final TaskInstanceDao dao = mock(TaskInstanceDao.class);
+ private final ITaskGroupCoordinator taskGroup =
mock(ITaskGroupCoordinator.class);
+ private final TaskExecutorClient client =
mock(TaskExecutorClient.class);
+ private final WorkerGroupDispatcherCoordinator dispatcher =
mock(WorkerGroupDispatcherCoordinator.class);
+ private final ITaskExecution task = mock(ITaskExecution.class);
+ private final IWorkflowExecution workflow =
mock(IWorkflowExecution.class);
+ private final IWorkflowExecutionGraph graph =
mock(IWorkflowExecutionGraph.class);
+ private final WorkflowEventBus bus = spy(new WorkflowEventBus());
+ private final TaskInstance shared = new TaskInstance();
+ private final TaskInstance persisted = new TaskInstance();
+ private final AtomicBoolean fail = new AtomicBoolean();
+ private final AtomicInteger attempts = new AtomicInteger();
+ private final EventType type;
+ private final TaskStateActionFactory factory;
+ private Runnable deliver;
+ private AbstractTaskLifecycleEventHandler<?> handler;
+ private AbstractTaskLifecycleEvent event;
+
+ private Fixture(final EventType type) {
+ this.type = type;
+ shared.setId(1);
+ shared.setName("task-persistence-recovery");
+ shared.setState(type == EventType.DISPATCHED || type ==
EventType.DELAY
+ ? TaskExecutionStatus.SUBMITTED_SUCCESS
+ : type == EventType.RUNNING ? TaskExecutionStatus.DISPATCH
: TaskExecutionStatus.RUNNING_EXECUTION);
+ shared.setHost("old-worker:1234");
+ shared.setLogPath("old-log");
+ shared.setAppLink("old-context");
+ shared.setVarPool("[]");
+ shared.setFirstSubmitTime(new Date());
+ shared.setDelayTime(10);
+ BeanUtils.copyProperties(shared, persisted);
+ final WorkflowInstance workflowInstance = new WorkflowInstance();
+ workflowInstance.setId(1);
+ workflowInstance.setVarPool("[]");
+ when(task.getTaskInstance()).thenReturn(shared);
+ when(task.getId()).thenReturn(1);
+ when(task.getWorkflowEventBus()).thenReturn(bus);
+ when(task.getWorkflowExecutionGraph()).thenReturn(graph);
+ when(workflow.getWorkflowInstance()).thenReturn(workflowInstance);
+ when(workflow.getWorkflowEventBus()).thenReturn(bus);
+
when(dao.updateById(any(TaskInstance.class))).thenAnswer(invocation -> {
+ attempts.incrementAndGet();
+ if (fail.get()) {
+ throw new DataAccessResourceFailureException("synthetic
task update failure");
+ }
+ BeanUtils.copyProperties(invocation.getArgument(0), persisted);
+ return true;
+ });
+ final List<ITaskStateAction> actions = new ArrayList<>();
+ final List<AbstractTaskStateAction> realActions =
Arrays.asList(new TaskSubmittedStateAction(),
+ new TaskDispatchStateAction(), new
TaskRunningStateAction(), new TaskPauseStateAction(),
+ new TaskKillStateAction(), new TaskFailureStateAction(),
new TaskDelayExecutionStateAction());
+ for (TaskExecutionStatus state : TaskExecutionStatus.values()) {
+ final AbstractTaskStateAction real =
realActions.stream().filter(a -> a.matchState() == state)
+ .findFirst().orElse(null);
+ if (real != null) {
+ real.taskInstanceDao = dao;
+ real.taskGroupCoordinator = taskGroup;
+ if (real instanceof TaskSubmittedStateAction) {
+ // Also initialize the shadowing field on the
unmodified implementation for the red test.
+ ReflectionTestUtils.setField(real, "taskInstanceDao",
dao);
+ ReflectionTestUtils.setField(real,
"workerGroupDispatcherCoordinator", dispatcher);
+ }
+ actions.add(real);
+ } else {
+ final ITaskStateAction action =
mock(ITaskStateAction.class);
+ when(action.matchState()).thenReturn(state);
+ actions.add(action);
+ }
+ }
+ factory = new TaskStateActionFactory(actions);
+ switch (type) {
+ case DISPATCHED:
+ bind(new TaskDispatchedLifecycleEventHandler(client),
+ new TaskDispatchedLifecycleEvent(task,
"new-worker:1234"));
+ break;
+ case RUNNING:
+ bind(new TaskRunningLifecycleEventHandler(client),
+ new TaskRunningLifecycleEvent(task, "new-log",
EVENT_TIME));
+ break;
+ case PAUSED:
+ bind(new TaskPausedLifecycleEventHandler(client),
TaskPausedLifecycleEvent.of(task));
+ break;
+ case KILLED:
+ bind(new TaskKilledLifecycleEventHandler(client), new
TaskKilledLifecycleEvent(task, EVENT_TIME));
+ break;
+ case FAILED:
+ bind(new TaskFailedLifecycleEventHandler(client), new
TaskFailedLifecycleEvent(task, EVENT_TIME));
+ break;
+ case FATAL:
+ bind(new TaskFatalLifecycleEventHandler(), new
TaskFatalLifecycleEvent(task, EVENT_TIME));
+ break;
+ case CONTEXT:
+ bind(new
TaskRuntimeContextChangedLifecycleEventHandler(client),
+ new TaskRuntimeContextChangedEvent(task,
"new-context"));
+ break;
+ case DELAY:
+ bind(new TaskDispatchLifecycleEventHandler(),
TaskDispatchLifecycleEvent.of(task));
+ break;
+ default:
+ throw new IllegalArgumentException(type.name());
+ }
+ }
+
+ private <T extends AbstractTaskLifecycleEvent> void bind(final
AbstractTaskLifecycleEventHandler<T> handler,
+ final T
event) {
+ this.handler = handler;
+ this.event = event;
+ ReflectionTestUtils.setField(handler, "taskStateActionFactory",
factory);
+ deliver = () -> handler.handle(workflow, event);
+ }
+
+ private TaskGroupQueueDao useRealTaskGroupCoordinator() {
+ shared.setTaskGroupId(1);
+ final TaskGroupQueueDao queueDao = mock(TaskGroupQueueDao.class);
+ final TaskGroupCoordinator coordinator = new
TaskGroupCoordinator();
+ ReflectionTestUtils.setField(coordinator, "taskGroupQueueDao",
queueDao);
+ ((AbstractTaskStateAction) factory
+
.getTaskStateAction(TaskExecutionStatus.RUNNING_EXECUTION)).taskGroupCoordinator
= coordinator;
+ return queueDao;
+ }
+
+ private TaskExecutionStatus expectedState() {
+ switch (type) {
+ case DISPATCHED:
+ return TaskExecutionStatus.DISPATCH;
+ case PAUSED:
+ return TaskExecutionStatus.PAUSE;
+ case KILLED:
+ return TaskExecutionStatus.KILL;
+ case FAILED:
+ case FATAL:
+ return TaskExecutionStatus.FAILURE;
+ case DELAY:
+ return TaskExecutionStatus.DELAY_EXECUTION;
+ default:
+ return TaskExecutionStatus.RUNNING_EXECUTION;
+ }
+ }
+
+ private void assertPersistedResult() {
+ assertThat(shared.getState()).isEqualTo(expectedState());
+ assertThat(fields(persisted)).isEqualTo(fields(shared));
+ if (type == EventType.DISPATCHED) {
+ assertThat(shared.getHost()).isEqualTo("new-worker:1234");
+ } else if (type == EventType.RUNNING) {
+ assertThat(shared.getStartTime()).isEqualTo(EVENT_TIME);
+ assertThat(shared.getLogPath()).isEqualTo("new-log");
+ } else if (type == EventType.CONTEXT) {
+ assertThat(shared.getAppLink()).isEqualTo("new-context");
+ } else if (type == EventType.KILLED || type == EventType.FAILED ||
type == EventType.FATAL) {
+ assertThat(shared.getEndTime()).isEqualTo(EVENT_TIME);
+ } else if (type == EventType.PAUSED) {
+ assertThat(shared.getEndTime()).isNull();
+ verify(graph).markTaskExecutionChainPause(task);
+ } else if (type == EventType.DELAY) {
+ verify(dispatcher).dispatchTask(eq(task), anyLong());
+ }
+ }
+
+ private void assertAck(final int count) {
+ verify(client,
times(count)).ackTaskExecutorLifecycleEvent(eq(task),
+
any(ITaskExecutorLifecycleEventReporter.TaskExecutorLifecycleEventAck.class));
+ }
+ }
+}
diff --git
a/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/engine/task/statemachine/TaskSuccessPersistenceRecoveryTest.java
b/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/engine/task/statemachine/TaskSuccessPersistenceRecoveryTest.java
new file mode 100644
index 0000000000..dc368d9a63
--- /dev/null
+++
b/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/engine/task/statemachine/TaskSuccessPersistenceRecoveryTest.java
@@ -0,0 +1,564 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.dolphinscheduler.server.master.engine.task.statemachine;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.apache.dolphinscheduler.dao.entity.TaskInstance;
+import org.apache.dolphinscheduler.dao.entity.WorkflowInstance;
+import org.apache.dolphinscheduler.dao.repository.TaskInstanceDao;
+import org.apache.dolphinscheduler.plugin.task.api.enums.DataType;
+import org.apache.dolphinscheduler.plugin.task.api.enums.Direct;
+import org.apache.dolphinscheduler.plugin.task.api.enums.TaskExecutionStatus;
+import org.apache.dolphinscheduler.plugin.task.api.model.Property;
+import org.apache.dolphinscheduler.plugin.task.api.utils.VarPoolUtils;
+import org.apache.dolphinscheduler.server.master.engine.AbstractLifecycleEvent;
+import org.apache.dolphinscheduler.server.master.engine.ILifecycleEventHandler;
+import org.apache.dolphinscheduler.server.master.engine.ILifecycleEventType;
+import org.apache.dolphinscheduler.server.master.engine.ITaskGroupCoordinator;
+import org.apache.dolphinscheduler.server.master.engine.WorkflowEventBus;
+import
org.apache.dolphinscheduler.server.master.engine.WorkflowEventBusFireWorker;
+import
org.apache.dolphinscheduler.server.master.engine.task.client.PhysicalTaskExecutorClientDelegator;
+import
org.apache.dolphinscheduler.server.master.engine.task.client.TaskExecutorClient;
+import
org.apache.dolphinscheduler.server.master.engine.task.execution.ITaskExecution;
+import
org.apache.dolphinscheduler.server.master.engine.task.lifecycle.event.TaskSuccessLifecycleEvent;
+import
org.apache.dolphinscheduler.server.master.engine.task.lifecycle.handler.TaskSuccessLifecycleEventHandler;
+import
org.apache.dolphinscheduler.server.master.engine.workflow.execution.IWorkflowExecution;
+import
org.apache.dolphinscheduler.server.master.engine.workflow.lifecycle.WorkflowLifecycleEventType;
+import
org.apache.dolphinscheduler.server.master.engine.workflow.lifecycle.event.WorkflowTopologyLogicalTransitionWithTaskFinishLifecycleEvent;
+import
org.apache.dolphinscheduler.server.master.runner.IWorkflowExecuteContext;
+import org.apache.dolphinscheduler.service.alert.WorkflowAlertManager;
+import
org.apache.dolphinscheduler.task.executor.eventbus.ITaskExecutorLifecycleEventReporter;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.dao.DataAccessResourceFailureException;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import com.google.common.collect.ImmutableMap;
+
+@ExtendWith(MockitoExtension.class)
+class TaskSuccessPersistenceRecoveryTest {
+
+ @Mock
+ private TaskInstanceDao taskInstanceDao;
+
+ @Mock
+ private ITaskGroupCoordinator taskGroupCoordinator;
+
+ @Mock
+ private TaskExecutorClient taskExecutorClient;
+
+ @Mock
+ private WorkflowAlertManager workflowAlertManager;
+
+ @Mock
+ private ITaskExecution taskExecution;
+
+ @Mock
+ private IWorkflowExecution workflowExecution;
+
+ private WorkflowEventBus workflowEventBus;
+
+ private final AtomicBoolean failPersistence = new AtomicBoolean();
+
+ private final AtomicInteger persistenceAttempts = new AtomicInteger();
+
+ private final PersistentTaskSnapshot persistentTaskSnapshot = new
PersistentTaskSnapshot();
+
+ private TaskInstance taskInstance;
+
+ private TaskSuccessLifecycleEvent successEvent;
+
+ private TestableTaskSuccessLifecycleEventHandler handler;
+
+ private TaskStateActionFactory taskStateActionFactory;
+
+ @BeforeEach
+ void setUp() {
+ taskInstance = new TaskInstance();
+ taskInstance.setId(1);
+ taskInstance.setName("terminal-state-recovery");
+ taskInstance.setState(TaskExecutionStatus.RUNNING_EXECUTION);
+ taskInstance.setVarPool("[]");
+ persistentTaskSnapshot.copyFrom(taskInstance);
+
+ final WorkflowInstance workflowInstance = new WorkflowInstance();
+ workflowInstance.setId(1);
+ workflowInstance.setName("terminal-state-recovery-workflow");
+ workflowInstance.setVarPool("[]");
+
+ workflowEventBus = spy(new WorkflowEventBus());
+
+ lenient().when(taskExecution.getId()).thenReturn(1);
+
lenient().when(taskExecution.getName()).thenReturn(taskInstance.getName());
+ when(taskExecution.getTaskInstance()).thenReturn(taskInstance);
+
lenient().when(taskExecution.getWorkflowEventBus()).thenReturn(workflowEventBus);
+
lenient().when(workflowExecution.getWorkflowInstance()).thenReturn(workflowInstance);
+
lenient().when(workflowExecution.getWorkflowEventBus()).thenReturn(workflowEventBus);
+
+
lenient().when(taskInstanceDao.updateById(any(TaskInstance.class))).thenAnswer(invocation
-> {
+ persistenceAttempts.incrementAndGet();
+ if (failPersistence.get()) {
+ throw new DataAccessResourceFailureException("synthetic
database write failure");
+ }
+ persistentTaskSnapshot.copyFrom(invocation.getArgument(0));
+ return true;
+ });
+
+ final TaskRunningStateAction runningStateAction = new
TaskRunningStateAction();
+ runningStateAction.taskGroupCoordinator = taskGroupCoordinator;
+ runningStateAction.taskInstanceDao = taskInstanceDao;
+
+ final TaskSuccessStateAction successStateAction = new
TaskSuccessStateAction();
+ successStateAction.taskGroupCoordinator = taskGroupCoordinator;
+ successStateAction.taskInstanceDao = taskInstanceDao;
+
+ taskStateActionFactory = new TaskStateActionFactory(
+ createStateActions(runningStateAction, successStateAction));
+
+ handler = new
TestableTaskSuccessLifecycleEventHandler(taskExecutorClient,
workflowAlertManager);
+ handler.setTaskStateActionFactory(taskStateActionFactory);
+
+ successEvent = TaskSuccessLifecycleEvent.builder()
+ .taskExecution(taskExecution)
+ .endTime(new Date(1_791_964_800_123L))
+ .varPool(Collections.emptyList())
+ .needAlert(false)
+ .build();
+ }
+
+ @Test
+ void shouldPersistAndAcknowledgeSuccessWithoutFailure() {
+ handler.handle(workflowExecution, successEvent);
+
+
assertThat(taskInstance.getState()).isEqualTo(TaskExecutionStatus.SUCCESS);
+
assertThat(persistentTaskSnapshot.state).isEqualTo(TaskExecutionStatus.SUCCESS);
+
assertThat(persistentTaskSnapshot.endTime).isEqualTo(successEvent.getEndTime());
+ assertThat(persistenceAttempts).hasValue(1);
+ verify(workflowEventBus).publish(any(AbstractLifecycleEvent.class));
+ verifyAck(1);
+ }
+
+ @Test
+ void shouldKeepInMemoryStateRetryableWhenFirstPersistenceFails() {
+ failPersistence.set(true);
+
+ assertThatThrownBy(() -> handler.handle(workflowExecution,
successEvent))
+ .isInstanceOf(DataAccessResourceFailureException.class);
+
+
assertThat(persistentTaskSnapshot.state).isEqualTo(TaskExecutionStatus.RUNNING_EXECUTION);
+
assertThat(taskInstance.getState()).isEqualTo(TaskExecutionStatus.RUNNING_EXECUTION);
+ assertThat(taskInstance.getEndTime()).isNull();
+ assertThat(taskInstance.getVarPool()).isEqualTo("[]");
+ assertThat(persistenceAttempts).hasValue(1);
+ verifyAck(0);
+ }
+
+ @Test
+ void
shouldPersistAndAcknowledgeWhenSameSuccessEventIsReportedAfterRecovery() {
+ failPersistence.set(true);
+ assertThatThrownBy(() -> handler.handle(workflowExecution,
successEvent))
+ .isInstanceOf(DataAccessResourceFailureException.class);
+
+ failPersistence.set(false);
+ handler.handle(workflowExecution, successEvent);
+
+ assertThat(persistenceAttempts).hasValue(2);
+
assertThat(persistentTaskSnapshot.state).isEqualTo(TaskExecutionStatus.SUCCESS);
+
assertThat(persistentTaskSnapshot.endTime).isEqualTo(successEvent.getEndTime());
+ verify(workflowEventBus).publish(any(AbstractLifecycleEvent.class));
+ verifyAck(1);
+ }
+
+ @Test
+ void
shouldNotAcknowledgeRepeatedSuccessReportsWhilePersistenceKeepsFailing() {
+ failPersistence.set(true);
+
+ assertThatThrownBy(() -> handler.handle(workflowExecution,
successEvent))
+ .isInstanceOf(DataAccessResourceFailureException.class);
+ assertThatThrownBy(() -> handler.handle(workflowExecution,
successEvent))
+ .isInstanceOf(DataAccessResourceFailureException.class);
+
+ assertThat(persistenceAttempts).hasValue(2);
+
assertThat(persistentTaskSnapshot.state).isEqualTo(TaskExecutionStatus.RUNNING_EXECUTION);
+ verifyAck(0);
+ }
+
+ @Test
+ void shouldAcknowledgeDuplicateSuccessAfterItWasPersisted() {
+ handler.handle(workflowExecution, successEvent);
+ clearInvocations(taskExecutorClient, workflowEventBus);
+
+ handler.handle(workflowExecution, successEvent);
+
+ assertThat(persistenceAttempts).hasValue(1);
+
assertThat(persistentTaskSnapshot.state).isEqualTo(TaskExecutionStatus.SUCCESS);
+ verify(workflowEventBus,
never()).publish(any(AbstractLifecycleEvent.class));
+ verifyAck(1);
+ }
+
+ @Test
+ void
shouldRetryRecognizedConnectionFailureThroughWorkflowEventBusAndPersistAfterRecovery()
{
+ final IWorkflowExecuteContext workflowExecuteContext =
mock(IWorkflowExecuteContext.class);
+ final WorkflowInstance workflowInstance =
workflowExecution.getWorkflowInstance();
+
when(workflowExecuteContext.getWorkflowInstance()).thenReturn(workflowInstance);
+
when(workflowExecution.getWorkflowExecuteContext()).thenReturn(workflowExecuteContext);
+ when(workflowExecution.getId()).thenReturn(1);
+
when(workflowExecution.getName()).thenReturn("terminal-state-recovery-workflow");
+
+ final WorkflowEventBusFireWorker fireWorker = new
WorkflowEventBusFireWorker();
+ fireWorker.registerWorkflowEventBus(workflowExecution);
+ fireWorker.registerEventHandler(handler);
+ fireWorker.registerEventHandler(new
NoOpWorkflowTopologyEventHandler());
+ workflowEventBus.publish(successEvent);
+
+ failPersistence.set(true);
+ fireWorker.fireAllRegisteredEvent();
+
+
assertThat(taskInstance.getState()).isEqualTo(TaskExecutionStatus.RUNNING_EXECUTION);
+
assertThat(persistentTaskSnapshot.state).isEqualTo(TaskExecutionStatus.RUNNING_EXECUTION);
+ assertThat(persistenceAttempts).hasValue(1);
+ assertThat(workflowEventBus.isEmpty()).isFalse();
+ verifyAck(0);
+
+ failPersistence.set(false);
+ fireWorker.fireAllRegisteredEvent();
+
+
assertThat(taskInstance.getState()).isEqualTo(TaskExecutionStatus.SUCCESS);
+
assertThat(persistentTaskSnapshot.state).isEqualTo(TaskExecutionStatus.SUCCESS);
+
assertThat(persistentTaskSnapshot.endTime).isEqualTo(successEvent.getEndTime());
+ assertThat(persistenceAttempts).hasValue(2);
+ assertThat(workflowEventBus.isEmpty()).isTrue();
+ verifyAck(1);
+ }
+
+ @Test
+ void
shouldPreserveAndMergeOutputVariablesAcrossPersistenceFailureAndRetransmission()
{
+ final String originalTaskVarPool = serializeProperties(
+ property("existing", "old"),
+ property("result", "before"));
+ final String originalWorkflowVarPool =
serializeProperties(property("workflow", "kept"));
+ taskInstance.setVarPool(originalTaskVarPool);
+ persistentTaskSnapshot.copyFrom(taskInstance);
+
workflowExecution.getWorkflowInstance().setVarPool(originalWorkflowVarPool);
+ successEvent = successEventWithVariables(
+ property("result", "after"),
+ property("output", "new"));
+
+ failPersistence.set(true);
+ assertThatThrownBy(() -> handler.handle(workflowExecution,
successEvent))
+ .isInstanceOf(DataAccessResourceFailureException.class);
+
+
assertThat(taskInstance.getState()).isEqualTo(TaskExecutionStatus.RUNNING_EXECUTION);
+ assertThat(taskInstance.getEndTime()).isNull();
+ assertThat(taskInstance.getVarPool()).isEqualTo(originalTaskVarPool);
+
assertThat(persistentTaskSnapshot.state).isEqualTo(TaskExecutionStatus.RUNNING_EXECUTION);
+
assertThat(persistentTaskSnapshot.varPool).isEqualTo(originalTaskVarPool);
+
assertThat(workflowExecution.getWorkflowInstance().getVarPool()).isEqualTo(originalWorkflowVarPool);
+ verify(workflowEventBus,
never()).publish(any(AbstractLifecycleEvent.class));
+ verifyAck(0);
+
+ failPersistence.set(false);
+ handler.handle(workflowExecution, successEvent);
+
+ assertThat(persistenceAttempts).hasValue(2);
+
assertThat(taskInstance.getState()).isEqualTo(TaskExecutionStatus.SUCCESS);
+
assertThat(taskInstance.getEndTime()).isEqualTo(successEvent.getEndTime());
+ assertVarPool(taskInstance.getVarPool(), ImmutableMap.of(
+ "existing", "old",
+ "result", "after",
+ "output", "new"));
+ assertVarPool(persistentTaskSnapshot.varPool, ImmutableMap.of(
+ "existing", "old",
+ "result", "after",
+ "output", "new"));
+ assertVarPool(workflowExecution.getWorkflowInstance().getVarPool(),
ImmutableMap.of(
+ "workflow", "kept",
+ "existing", "old",
+ "result", "after",
+ "output", "new"));
+
verify(workflowEventBus).publish(any(WorkflowTopologyLogicalTransitionWithTaskFinishLifecycleEvent.class));
+ verifyAck(1);
+
+ clearInvocations(taskExecutorClient, workflowEventBus);
+ handler.handle(workflowExecution, successEvent);
+
+ assertThat(persistenceAttempts).hasValue(2);
+ assertVarPool(taskInstance.getVarPool(), ImmutableMap.of(
+ "existing", "old",
+ "result", "after",
+ "output", "new"));
+ assertVarPool(workflowExecution.getWorkflowInstance().getVarPool(),
ImmutableMap.of(
+ "workflow", "kept",
+ "existing", "old",
+ "result", "after",
+ "output", "new"));
+ verify(workflowEventBus,
never()).publish(any(AbstractLifecycleEvent.class));
+ verifyAck(1);
+ }
+
+ @Test
+ void shouldPersistThenPublishTopologyThenAcknowledge() {
+ taskInstance.setVarPool(serializeProperties(property("existing",
"old")));
+ persistentTaskSnapshot.copyFrom(taskInstance);
+ successEvent = successEventWithVariables(property("output", "new"));
+ final List<String> actions = new ArrayList<>();
+
+
when(taskInstanceDao.updateById(any(TaskInstance.class))).thenAnswer(invocation
-> {
+ persistenceAttempts.incrementAndGet();
+ final TaskInstance taskInstanceToUpdate =
invocation.getArgument(0);
+ assertThat(taskInstanceToUpdate).isNotSameAs(taskInstance);
+
assertThat(taskInstance.getState()).isEqualTo(TaskExecutionStatus.RUNNING_EXECUTION);
+ assertThat(taskInstance.getEndTime()).isNull();
+ assertVarPool(taskInstance.getVarPool(),
ImmutableMap.of("existing", "old"));
+
assertThat(taskInstanceToUpdate.getState()).isEqualTo(TaskExecutionStatus.SUCCESS);
+
assertThat(taskInstanceToUpdate.getEndTime()).isEqualTo(successEvent.getEndTime());
+ assertVarPool(taskInstanceToUpdate.getVarPool(),
ImmutableMap.of("existing", "old", "output", "new"));
+ actions.add("persist-start");
+ persistentTaskSnapshot.copyFrom(taskInstanceToUpdate);
+ actions.add("persisted");
+ return true;
+ });
+ doAnswer(invocation -> {
+ assertThat((Object) invocation.getArgument(0))
+
.isInstanceOf(WorkflowTopologyLogicalTransitionWithTaskFinishLifecycleEvent.class);
+
assertThat(persistentTaskSnapshot.state).isEqualTo(TaskExecutionStatus.SUCCESS);
+
assertThat(taskInstance.getState()).isEqualTo(TaskExecutionStatus.SUCCESS);
+ actions.add("topology");
+ return invocation.callRealMethod();
+ }).when(workflowEventBus).publish(any(AbstractLifecycleEvent.class));
+ doAnswer(invocation -> {
+
assertThat(persistentTaskSnapshot.state).isEqualTo(TaskExecutionStatus.SUCCESS);
+
assertThat(taskInstance.getState()).isEqualTo(TaskExecutionStatus.SUCCESS);
+
assertThat(taskInstance.getEndTime()).isEqualTo(successEvent.getEndTime());
+ assertVarPool(taskInstance.getVarPool(),
ImmutableMap.of("existing", "old", "output", "new"));
+ actions.add("ack");
+ return null;
+ }).when(taskExecutorClient).ackTaskExecutorLifecycleEvent(
+ eq(taskExecution),
any(ITaskExecutorLifecycleEventReporter.TaskExecutorLifecycleEventAck.class));
+
+ handler.handle(workflowExecution, successEvent);
+
+ assertThat(actions).containsExactly("persist-start", "persisted",
"topology", "ack");
+ }
+
+ @Test
+ void
shouldKeepPersistedSuccessWhenRealAckClientFailsAndAcknowledgeRetransmission() {
+ taskInstance.setTaskType("SHELL");
+ taskInstance.setHost("worker.test:1234");
+ taskInstance.setVarPool(serializeProperties(property("existing",
"old")));
+ persistentTaskSnapshot.copyFrom(taskInstance);
+ successEvent = successEventWithVariables(property("output", "new"));
+
+ final PhysicalTaskExecutorClientDelegator delegator =
mock(PhysicalTaskExecutorClientDelegator.class);
+ final TaskExecutorClient realTaskExecutorClient = new
TaskExecutorClient();
+ ReflectionTestUtils.setField(realTaskExecutorClient,
"physicalTaskExecutorClientDelegator", delegator);
+ doThrow(new RuntimeException("synthetic ACK send failure"))
+ .doNothing()
+ .when(delegator)
+ .ackTaskExecutorLifecycleEvent(
+ eq(taskExecution),
+
any(ITaskExecutorLifecycleEventReporter.TaskExecutorLifecycleEventAck.class));
+ handler = new
TestableTaskSuccessLifecycleEventHandler(realTaskExecutorClient,
workflowAlertManager);
+ handler.setTaskStateActionFactory(taskStateActionFactory);
+
+ handler.handle(workflowExecution, successEvent);
+
+
assertThat(persistentTaskSnapshot.state).isEqualTo(TaskExecutionStatus.SUCCESS);
+
assertThat(taskInstance.getState()).isEqualTo(TaskExecutionStatus.SUCCESS);
+ assertVarPool(taskInstance.getVarPool(), ImmutableMap.of("existing",
"old", "output", "new"));
+ assertThat(persistenceAttempts).hasValue(1);
+
verify(workflowEventBus).publish(any(WorkflowTopologyLogicalTransitionWithTaskFinishLifecycleEvent.class));
+
+ handler.handle(workflowExecution, successEvent);
+
+ assertThat(persistenceAttempts).hasValue(1);
+
assertThat(taskInstance.getState()).isEqualTo(TaskExecutionStatus.SUCCESS);
+ assertVarPool(taskInstance.getVarPool(), ImmutableMap.of("existing",
"old", "output", "new"));
+ verify(workflowEventBus, times(1))
+
.publish(any(WorkflowTopologyLogicalTransitionWithTaskFinishLifecycleEvent.class));
+ verify(delegator, times(2)).ackTaskExecutorLifecycleEvent(
+ eq(taskExecution),
any(ITaskExecutorLifecycleEventReporter.TaskExecutorLifecycleEventAck.class));
+ }
+
+ @Test
+ void shouldConvergeWhenFirstUpdateCommitsButThrowsToCaller() {
+ taskInstance.setVarPool(serializeProperties(property("existing",
"old")));
+ persistentTaskSnapshot.copyFrom(taskInstance);
+ successEvent = successEventWithVariables(property("output", "new"));
+ final AtomicInteger localAttempts = new AtomicInteger();
+
when(taskInstanceDao.updateById(any(TaskInstance.class))).thenAnswer(invocation
-> {
+ persistenceAttempts.incrementAndGet();
+ localAttempts.incrementAndGet();
+ persistentTaskSnapshot.copyFrom(invocation.getArgument(0));
+ if (localAttempts.get() == 1) {
+ throw new DataAccessResourceFailureException("synthetic
uncertain commit result");
+ }
+ return true;
+ });
+
+ assertThatThrownBy(() -> handler.handle(workflowExecution,
successEvent))
+ .isInstanceOf(DataAccessResourceFailureException.class);
+
+
assertThat(persistentTaskSnapshot.state).isEqualTo(TaskExecutionStatus.SUCCESS);
+
assertThat(taskInstance.getState()).isEqualTo(TaskExecutionStatus.RUNNING_EXECUTION);
+ assertThat(taskInstance.getEndTime()).isNull();
+ assertVarPool(taskInstance.getVarPool(), ImmutableMap.of("existing",
"old"));
+ verify(workflowEventBus,
never()).publish(any(AbstractLifecycleEvent.class));
+ verifyAck(0);
+
+ handler.handle(workflowExecution, successEvent);
+
+ assertThat(persistenceAttempts).hasValue(2);
+
assertThat(taskInstance.getState()).isEqualTo(TaskExecutionStatus.SUCCESS);
+
assertThat(taskInstance.getEndTime()).isEqualTo(successEvent.getEndTime());
+ assertVarPool(taskInstance.getVarPool(), ImmutableMap.of("existing",
"old", "output", "new"));
+ assertVarPool(persistentTaskSnapshot.varPool,
ImmutableMap.of("existing", "old", "output", "new"));
+
verify(workflowEventBus).publish(any(WorkflowTopologyLogicalTransitionWithTaskFinishLifecycleEvent.class));
+ verifyAck(1);
+ }
+
+ private TaskSuccessLifecycleEvent successEventWithVariables(final
Property... properties) {
+ return TaskSuccessLifecycleEvent.builder()
+ .taskExecution(taskExecution)
+ .endTime(new Date(1_791_964_800_123L))
+ .varPool(Arrays.asList(properties))
+ .needAlert(false)
+ .build();
+ }
+
+ private static Property property(final String name, final String value) {
+ return new Property(name, Direct.OUT, DataType.VARCHAR, value);
+ }
+
+ private static String serializeProperties(final Property... properties) {
+ return VarPoolUtils.serializeVarPool(Arrays.asList(properties));
+ }
+
+ private static void assertVarPool(final String actualJson, final
Map<String, String> expectedValues) {
+ final Map<String, Property> propertiesByName =
VarPoolUtils.deserializeVarPool(actualJson)
+ .stream()
+ .collect(Collectors.toMap(Property::getProp,
Function.identity()));
+ assertThat(propertiesByName).containsOnlyKeys(expectedValues.keySet());
+ expectedValues.forEach((name, value) -> {
+ final Property property = propertiesByName.get(name);
+ assertThat(property.getValue()).as("value of %s",
name).isEqualTo(value);
+ assertThat(property.getDirect()).as("direct of %s",
name).isEqualTo(Direct.OUT);
+ assertThat(property.getType()).as("type of %s",
name).isEqualTo(DataType.VARCHAR);
+ });
+ }
+
+ private List<ITaskStateAction> createStateActions(final
TaskRunningStateAction runningStateAction,
+ final
TaskSuccessStateAction successStateAction) {
+ return Arrays.stream(TaskExecutionStatus.values())
+ .map(state -> {
+ if (state == TaskExecutionStatus.RUNNING_EXECUTION) {
+ return runningStateAction;
+ }
+ if (state == TaskExecutionStatus.SUCCESS) {
+ return successStateAction;
+ }
+ final ITaskStateAction stateAction =
mock(ITaskStateAction.class);
+ when(stateAction.matchState()).thenReturn(state);
+ return stateAction;
+ })
+ .collect(Collectors.toList());
+ }
+
+ private void verifyAck(final int expectedInvocations) {
+ if (expectedInvocations == 0) {
+ verify(taskExecutorClient, never()).ackTaskExecutorLifecycleEvent(
+ eq(taskExecution),
any(ITaskExecutorLifecycleEventReporter.TaskExecutorLifecycleEventAck.class));
+ return;
+ }
+ verify(taskExecutorClient).ackTaskExecutorLifecycleEvent(
+ eq(taskExecution),
any(ITaskExecutorLifecycleEventReporter.TaskExecutorLifecycleEventAck.class));
+ }
+
+ private static final class PersistentTaskSnapshot {
+
+ private TaskExecutionStatus state;
+
+ private Date endTime;
+
+ private String varPool;
+
+ private void copyFrom(final TaskInstance source) {
+ state = source.getState();
+ endTime = source.getEndTime() == null ? null : new
Date(source.getEndTime().getTime());
+ varPool = source.getVarPool();
+ }
+
+ }
+
+ private static final class NoOpWorkflowTopologyEventHandler
+ implements
+ ILifecycleEventHandler<AbstractLifecycleEvent> {
+
+ @Override
+ public void handle(final IWorkflowExecution workflowExecution, final
AbstractLifecycleEvent event) {
+ // The task success transition is the behavior under test;
draining its downstream event keeps the bus
+ // finite.
+ }
+
+ @Override
+ public ILifecycleEventType matchEventType() {
+ return
WorkflowLifecycleEventType.TOPOLOGY_LOGICAL_TRANSACTION_WITH_TASK_FINISH;
+ }
+ }
+
+ private static final class TestableTaskSuccessLifecycleEventHandler
extends TaskSuccessLifecycleEventHandler {
+
+ private TestableTaskSuccessLifecycleEventHandler(final
TaskExecutorClient taskExecutorClient,
+ final
WorkflowAlertManager workflowAlertManager) {
+ super(taskExecutorClient, workflowAlertManager);
+ }
+
+ private void setTaskStateActionFactory(final TaskStateActionFactory
taskStateActionFactory) {
+ this.taskStateActionFactory = taskStateActionFactory;
+ }
+ }
+}
diff --git
a/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/integration/cases/TaskLifecyclePersistenceRecoveryTestCase.java
b/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/integration/cases/TaskLifecyclePersistenceRecoveryTestCase.java
new file mode 100644
index 0000000000..3e71d5f36b
--- /dev/null
+++
b/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/integration/cases/TaskLifecyclePersistenceRecoveryTestCase.java
@@ -0,0 +1,134 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.dolphinscheduler.server.master.integration.cases;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+
+import org.apache.dolphinscheduler.common.enums.WorkflowExecutionStatus;
+import org.apache.dolphinscheduler.dao.entity.TaskInstance;
+import org.apache.dolphinscheduler.dao.repository.TaskInstanceDao;
+import
org.apache.dolphinscheduler.extract.master.command.RunWorkflowCommandParam;
+import org.apache.dolphinscheduler.plugin.task.api.enums.TaskExecutionStatus;
+import
org.apache.dolphinscheduler.server.master.AbstractMasterIntegrationTestCase;
+import org.apache.dolphinscheduler.server.master.integration.WorkflowOperator;
+import
org.apache.dolphinscheduler.server.master.integration.WorkflowTestCaseContext;
+
+import java.time.Duration;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+import org.springframework.boot.test.mock.mockito.SpyBean;
+import org.springframework.dao.DataAccessResourceFailureException;
+
+public class TaskLifecyclePersistenceRecoveryTestCase extends
AbstractMasterIntegrationTestCase {
+
+ @SpyBean
+ private TaskInstanceDao taskInstanceDao;
+
+ @ParameterizedTest
+ @EnumSource(value = TaskExecutionStatus.class, names = {"DISPATCH",
"RUNNING_EXECUTION"})
+ void shouldCompleteWorkflowAfterNonTerminalUpdateRecovers(final
TaskExecutionStatus state) {
+ final FailureProbe probe = failFirstUpdate(state);
+ final Integer workflowId =
start("/it/start/workflow_with_two_serial_fake_task_success.yaml");
+ awaitWorkflow(workflowId, WorkflowExecutionStatus.SUCCESS,
TaskExecutionStatus.SUCCESS, 2);
+ assertThat(probe.attempts).hasValue(2);
+ assertThat(probe.beforeFailure.get().getState()).isNotEqualTo(state);
+ }
+
+ @Test
+ void shouldFinishStoppingWorkflowAfterKillUpdateRecovers() {
+ final FailureProbe probe = failFirstUpdate(TaskExecutionStatus.KILL);
+ final Integer workflowId =
start("/it/stop/workflow_with_one_fake_task_failed.yaml");
+
await().pollInterval(Duration.ofMillis(100)).atMost(Duration.ofSeconds(30))
+ .untilAsserted(() ->
assertThat(repository.queryTaskInstance(workflowId)).satisfiesExactly(
+ task ->
assertThat(task.getState()).isEqualTo(TaskExecutionStatus.RUNNING_EXECUTION)));
+
assertThat(workflowOperator.stopWorkflowInstance(workflowId).isSuccess()).isTrue();
+ awaitWorkflow(workflowId, WorkflowExecutionStatus.STOP,
TaskExecutionStatus.KILL, 1);
+ assertThat(probe.attempts).hasValue(2);
+
assertThat(probe.beforeFailure.get().getState()).isEqualTo(TaskExecutionStatus.RUNNING_EXECUTION);
+ }
+
+ @Test
+ void shouldFinishFailedWorkflowAfterFailureUpdateRecovers() {
+ final FailureProbe probe =
failFirstUpdate(TaskExecutionStatus.FAILURE);
+ final Integer workflowId =
start("/it/start/workflow_with_one_fake_task_failed.yaml");
+ awaitWorkflow(workflowId, WorkflowExecutionStatus.FAILURE,
TaskExecutionStatus.FAILURE, 1);
+ assertThat(probe.attempts).hasValue(2);
+
assertThat(probe.beforeFailure.get().getState()).isEqualTo(TaskExecutionStatus.RUNNING_EXECUTION);
+ }
+
+ @Test
+ void shouldFinishTasksSharingAGroupAfterSuccessUpdateRecovers() {
+ final FailureProbe probe =
failFirstUpdate(TaskExecutionStatus.SUCCESS);
+ final Integer workflowId =
start("/it/start/workflow_with_fake_tasks_using_task_group.yaml");
+ // The task group coordinator waits one minute before its first
scheduling round.
+ awaitWorkflow(workflowId, WorkflowExecutionStatus.SUCCESS,
TaskExecutionStatus.SUCCESS, 2,
+ Duration.ofMinutes(2));
+ assertThat(probe.attempts).hasValue(2);
+
assertThat(probe.beforeFailure.get().getState()).isEqualTo(TaskExecutionStatus.RUNNING_EXECUTION);
+ }
+
+ private Integer start(final String resource) {
+ final WorkflowTestCaseContext context =
workflowTestCaseContextFactory.initializeContextFromYaml(resource);
+ return
workflowOperator.manualTriggerWorkflow(WorkflowOperator.WorkflowTriggerDTO.builder()
+ .workflowDefinition(context.getOneWorkflow())
+ .runWorkflowCommandParam(new RunWorkflowCommandParam())
+ .build());
+ }
+
+ private void awaitWorkflow(final Integer workflowId, final
WorkflowExecutionStatus workflowState,
+ final TaskExecutionStatus taskState, final int
taskCount) {
+ awaitWorkflow(workflowId, workflowState, taskState, taskCount,
Duration.ofSeconds(45));
+ }
+
+ private void awaitWorkflow(final Integer workflowId, final
WorkflowExecutionStatus workflowState,
+ final TaskExecutionStatus taskState, final int
taskCount, final Duration timeout) {
+ await().atMost(timeout).untilAsserted(() -> {
+
assertThat(repository.queryWorkflowInstance(workflowId).getState()).isEqualTo(workflowState);
+
assertThat(repository.queryTaskInstance(workflowId)).hasSize(taskCount)
+ .allSatisfy(task ->
assertThat(task.getState()).isEqualTo(taskState));
+ masterContainer.assertAllResourceReleased();
+ });
+ }
+
+ private FailureProbe failFirstUpdate(final TaskExecutionStatus state) {
+ final FailureProbe probe = new FailureProbe();
+ doAnswer(invocation -> {
+ final TaskInstance candidate = invocation.getArgument(0);
+ if ("A".equals(candidate.getName()) && candidate.getState() ==
state
+ && probe.attempts.incrementAndGet() == 1) {
+
probe.beforeFailure.set(taskInstanceDao.queryById(candidate.getId()));
+ throw new DataAccessResourceFailureException("synthetic first
lifecycle update failure");
+ }
+ return invocation.callRealMethod();
+ }).when(taskInstanceDao).updateById(any(TaskInstance.class));
+ return probe;
+ }
+
+ private static final class FailureProbe {
+
+ private final AtomicInteger attempts = new AtomicInteger();
+ private final AtomicReference<TaskInstance> beforeFailure = new
AtomicReference<>();
+ }
+}
diff --git
a/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/integration/cases/TaskSuccessPersistenceRecoveryTestCase.java
b/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/integration/cases/TaskSuccessPersistenceRecoveryTestCase.java
new file mode 100644
index 0000000000..fb514fb913
--- /dev/null
+++
b/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/integration/cases/TaskSuccessPersistenceRecoveryTestCase.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.dolphinscheduler.server.master.integration.cases;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+
+import org.apache.dolphinscheduler.common.enums.WorkflowExecutionStatus;
+import org.apache.dolphinscheduler.dao.entity.TaskInstance;
+import org.apache.dolphinscheduler.dao.repository.TaskInstanceDao;
+import
org.apache.dolphinscheduler.extract.master.command.RunWorkflowCommandParam;
+import org.apache.dolphinscheduler.plugin.task.api.enums.TaskExecutionStatus;
+import
org.apache.dolphinscheduler.server.master.AbstractMasterIntegrationTestCase;
+import org.apache.dolphinscheduler.server.master.integration.WorkflowOperator;
+import
org.apache.dolphinscheduler.server.master.integration.WorkflowTestCaseContext;
+
+import java.time.Duration;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.mock.mockito.SpyBean;
+import org.springframework.dao.DataAccessResourceFailureException;
+
+public class TaskSuccessPersistenceRecoveryTestCase extends
AbstractMasterIntegrationTestCase {
+
+ @SpyBean
+ private TaskInstanceDao taskInstanceDao;
+
+ @Test
+ public void testSuccessPersistenceFailureRetriesAndCompletesWorkflow() {
+ final WorkflowTestCaseContext context =
workflowTestCaseContextFactory.initializeContextFromYaml(
+ "/it/start/workflow_with_two_serial_fake_task_success.yaml");
+ final AtomicInteger successUpdateAttempts = new AtomicInteger();
+ final AtomicReference<TaskInstance> persistedTaskBeforeFailure = new
AtomicReference<>();
+
+ // Fail only A's first success update. All other calls use the real
DAO and the isolated H2 database.
+ doAnswer(invocation -> {
+ final TaskInstance taskInstance = invocation.getArgument(0);
+ if ("A".equals(taskInstance.getName()) && taskInstance.getState()
== TaskExecutionStatus.SUCCESS
+ && successUpdateAttempts.incrementAndGet() == 1) {
+
persistedTaskBeforeFailure.set(taskInstanceDao.queryById(taskInstance.getId()));
+ throw new DataAccessResourceFailureException("synthetic first
success update failure");
+ }
+ return invocation.callRealMethod();
+ }).when(taskInstanceDao).updateById(any(TaskInstance.class));
+
+ final Integer workflowInstanceId =
workflowOperator.manualTriggerWorkflow(
+ WorkflowOperator.WorkflowTriggerDTO.builder()
+ .workflowDefinition(context.getOneWorkflow())
+ .runWorkflowCommandParam(new RunWorkflowCommandParam())
+ .build());
+
+ await()
+ .atMost(Duration.ofSeconds(30))
+ .untilAsserted(() -> {
+
assertThat(repository.queryWorkflowInstance(workflowInstanceId).getState())
+ .isEqualTo(WorkflowExecutionStatus.SUCCESS);
+
assertThat(repository.queryTaskInstance(workflowInstanceId))
+ .hasSize(2)
+ .extracting(TaskInstance::getName)
+ .containsExactly("A", "B");
+
assertThat(repository.queryTaskInstance(workflowInstanceId))
+ .allSatisfy(taskInstance -> {
+
assertThat(taskInstance.getState()).isEqualTo(TaskExecutionStatus.SUCCESS);
+
assertThat(taskInstance.getEndTime()).isNotNull();
+ });
+ });
+
+ assertThat(successUpdateAttempts).hasValue(2);
+ assertThat(persistedTaskBeforeFailure.get()).isNotNull();
+
assertThat(persistedTaskBeforeFailure.get().getState()).isEqualTo(TaskExecutionStatus.RUNNING_EXECUTION);
+ assertThat(persistedTaskBeforeFailure.get().getEndTime()).isNull();
+ masterContainer.assertAllResourceReleased();
+ }
+}