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 86edf18211 [Fix-18604][Master] Preserve forced-success state when
recovering failed workflows (#18608)
86edf18211 is described below
commit 86edf18211c552a7efcbc91df1336850f5bd573f
Author: Molin Wang <[email protected]>
AuthorDate: Wed Sep 2 10:34:19 2026 +0800
[Fix-18604][Master] Preserve forced-success state when recovering failed
workflows (#18608)
---
.../statemachine/TaskForceSuccessStateAction.java | 9 ++
.../TaskForceSuccessStateActionTest.java | 88 ++++++++++++
...WorkflowInstanceRecoverFailureTaskTestCase.java | 44 ++++++
...e_workflow_with_forced_success_predecessor.yaml | 154 +++++++++++++++++++++
4 files changed, 295 insertions(+)
diff --git
a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/task/statemachine/TaskForceSuccessStateAction.java
b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/task/statemachine/TaskForceSuccessStateAction.java
index 3c34c52f28..d2e8b7fe18 100644
---
a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/task/statemachine/TaskForceSuccessStateAction.java
+++
b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/engine/task/statemachine/TaskForceSuccessStateAction.java
@@ -18,6 +18,8 @@
package org.apache.dolphinscheduler.server.master.engine.task.statemachine;
import org.apache.dolphinscheduler.plugin.task.api.enums.TaskExecutionStatus;
+import
org.apache.dolphinscheduler.server.master.engine.task.execution.ITaskExecution;
+import
org.apache.dolphinscheduler.server.master.engine.task.lifecycle.event.TaskSuccessLifecycleEvent;
import lombok.extern.slf4j.Slf4j;
@@ -27,6 +29,13 @@ import org.springframework.stereotype.Component;
@Component
public class TaskForceSuccessStateAction extends TaskSuccessStateAction {
+ @Override
+ protected void persistentTaskInstanceSuccessEventToDB(final ITaskExecution
taskExecution,
+ final
TaskSuccessLifecycleEvent taskSuccessEvent) {
+ // FORCED_SUCCESS is already persisted by the API.
+ // Skip the SUCCESS update while retaining variable merging and
downstream transitions.
+ }
+
@Override
public TaskExecutionStatus matchState() {
return TaskExecutionStatus.FORCED_SUCCESS;
diff --git
a/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/engine/task/statemachine/TaskForceSuccessStateActionTest.java
b/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/engine/task/statemachine/TaskForceSuccessStateActionTest.java
new file mode 100644
index 0000000000..c07ab9920b
--- /dev/null
+++
b/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/engine/task/statemachine/TaskForceSuccessStateActionTest.java
@@ -0,0 +1,88 @@
+/*
+ * 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.mockito.ArgumentMatchers.any;
+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.TaskExecutionStatus;
+import org.apache.dolphinscheduler.server.master.engine.AbstractLifecycleEvent;
+import org.apache.dolphinscheduler.server.master.engine.ITaskGroupCoordinator;
+import org.apache.dolphinscheduler.server.master.engine.WorkflowEventBus;
+import
org.apache.dolphinscheduler.server.master.engine.task.execution.ITaskExecution;
+import
org.apache.dolphinscheduler.server.master.engine.task.lifecycle.event.TaskStartLifecycleEvent;
+import
org.apache.dolphinscheduler.server.master.engine.workflow.execution.IWorkflowExecution;
+
+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;
+
+@ExtendWith(MockitoExtension.class)
+class TaskForceSuccessStateActionTest {
+
+ private TaskForceSuccessStateAction taskForceSuccessStateAction;
+
+ @Mock
+ private ITaskGroupCoordinator taskGroupCoordinator;
+
+ @Mock
+ private TaskInstanceDao taskInstanceDao;
+
+ @Mock
+ private ITaskExecution taskExecution;
+
+ @Mock
+ private IWorkflowExecution workflowExecution;
+
+ @Mock
+ private WorkflowEventBus workflowEventBus;
+
+ private TaskInstance taskInstance;
+
+ @BeforeEach
+ void setUp() {
+ taskForceSuccessStateAction = new TaskForceSuccessStateAction();
+ taskForceSuccessStateAction.taskGroupCoordinator =
taskGroupCoordinator;
+ taskForceSuccessStateAction.taskInstanceDao = taskInstanceDao;
+
+ taskInstance = new TaskInstance();
+ taskInstance.setState(TaskExecutionStatus.FORCED_SUCCESS);
+
+ when(taskExecution.getTaskInstance()).thenReturn(taskInstance);
+ when(taskExecution.getWorkflowEventBus()).thenReturn(workflowEventBus);
+ when(workflowExecution.getWorkflowInstance()).thenReturn(new
WorkflowInstance());
+ }
+
+ @Test
+ void shouldPreserveForcedSuccessWhileContinuingWorkflow() {
+ taskForceSuccessStateAction.onStartEvent(
+ workflowExecution,
+ taskExecution,
+ TaskStartLifecycleEvent.of(taskExecution));
+
+
assertThat(taskInstance.getState()).isEqualTo(TaskExecutionStatus.FORCED_SUCCESS);
+ verify(workflowEventBus).publish(any(AbstractLifecycleEvent.class));
+ }
+}
diff --git
a/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/integration/cases/WorkflowInstanceRecoverFailureTaskTestCase.java
b/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/integration/cases/WorkflowInstanceRecoverFailureTaskTestCase.java
index 90144c9007..99cb697c56 100644
---
a/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/integration/cases/WorkflowInstanceRecoverFailureTaskTestCase.java
+++
b/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/integration/cases/WorkflowInstanceRecoverFailureTaskTestCase.java
@@ -94,6 +94,50 @@ public class WorkflowInstanceRecoverFailureTaskTestCase
extends AbstractMasterIn
masterContainer.assertAllResourceReleased();
}
+ @Test
+ @DisplayName("Test recover failure tasks preserves forced-success task
state")
+ public void testRecoverFailureTasks_preservesForcedSuccessTaskState() {
+ final String yaml =
+
"/it/recover_failure_tasks/failure_workflow_with_forced_success_predecessor.yaml";
+ final WorkflowTestCaseContext context =
workflowTestCaseContextFactory.initializeContextFromYaml(yaml);
+
+ final Integer workflowInstanceId =
context.getWorkflowInstances().get(0).getId();
+ workflowOperator.recoverFailureTasks(workflowInstanceId);
+
+ await()
+ .atMost(Duration.ofMinutes(1))
+ .untilAsserted(() -> {
+ final WorkflowInstance workflowInstance =
repository.queryWorkflowInstance(workflowInstanceId);
+ assertThat(workflowInstance.getState())
+ .isEqualTo(WorkflowExecutionStatus.SUCCESS);
+ assertThat(workflowInstance.getRunTimes())
+ .isEqualTo(2);
+
+ final List<TaskInstance> taskInstances =
repository.queryTaskInstance(workflowInstanceId);
+ assertThat(taskInstances)
+ .hasSize(3);
+
+ assertThat(taskInstances)
+ .filteredOn(t -> "A".equals(t.getName()))
+ .singleElement()
+ .matches(t -> t.getState() ==
TaskExecutionStatus.FORCED_SUCCESS)
+ .matches(t -> t.getFlag() == Flag.YES);
+
+ assertThat(taskInstances)
+ .filteredOn(t -> "B".equals(t.getName()))
+ .anySatisfy(t -> {
+
assertThat(t.getState()).isEqualTo(TaskExecutionStatus.FAILURE);
+ assertThat(t.getFlag()).isEqualTo(Flag.NO);
+ })
+ .anySatisfy(t -> {
+
assertThat(t.getState()).isEqualTo(TaskExecutionStatus.SUCCESS);
+ assertThat(t.getFlag()).isEqualTo(Flag.YES);
+ assertThat(t.getLogPath()).isNotEmpty();
+ });
+ });
+ masterContainer.assertAllResourceReleased();
+ }
+
@Test
@DisplayName("Test recover a failure workflow from another master")
public void testRecoverFailureWorkflow_from_another_master() {
diff --git
a/dolphinscheduler-master/src/test/resources/it/recover_failure_tasks/failure_workflow_with_forced_success_predecessor.yaml
b/dolphinscheduler-master/src/test/resources/it/recover_failure_tasks/failure_workflow_with_forced_success_predecessor.yaml
new file mode 100644
index 0000000000..1e29530fa5
--- /dev/null
+++
b/dolphinscheduler-master/src/test/resources/it/recover_failure_tasks/failure_workflow_with_forced_success_predecessor.yaml
@@ -0,0 +1,154 @@
+#
+# 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.
+#
+project:
+ name: MasterIntegrationTest
+ code: 1
+ description: This is a fake project
+ userId: 1
+ userName: admin
+ createTime: 2024-08-12 00:00:00
+ updateTime: 2021-08-12 00:00:00
+
+workflowInstances:
+ - id: 1
+ name: workflow_with_forced_success_predecessor-20240816071251690
+ workflowDefinitionCode: 1
+ workflowDefinitionVersion: 1
+ projectCode: 1
+ state: FAILURE
+ recovery: NO
+ startTime: 2024-08-16 07:12:52
+ endTime: 2024-08-16 07:12:57
+ runTimes: 1
+ host: 127.0.0.1:5678
+ commandType: START_PROCESS
+ commandParam:
'{"commandType":"START_PROCESS","startNodes":[],"commandParams":[],"timeZone":"UTC"}'
+ taskDependType: TASK_POST
+ commandStartTime: 2024-08-16 07:12:52
+ isSubWorkflow: NO
+ executorId: 1
+ historyCmd: START_PROCESS
+ workerGroup: default
+ globalParams: '[]'
+ varPool: '[]'
+ dryRun: 0
+
+taskInstances:
+ - id: 1
+ name: A
+ taskType: LogicFakeTask
+ workflowInstanceId: 1
+ workflowInstanceName:
workflow_with_forced_success_predecessor-20240816071251690
+ projectCode: 1
+ taskCode: 1
+ taskDefinitionVersion: 1
+ state: FORCED_SUCCESS
+ firstSubmitTime: 2024-08-16 07:12:52
+ submitTime: 2024-08-16 07:12:53
+ startTime: 2024-08-16 07:12:53
+ endTime: 2024-08-16 07:12:54
+ retryTimes: 0
+ host: 127.0.0.1:1234
+ maxRetryTimes: 0
+ taskParams: '{"localParams":null,"varPool":[],"shellScript":"echo hello"}'
+ flag: YES
+ retryInterval: 0
+ delayTime: 0
+ workerGroup: default
+ executorId: 1
+ varPool: '[]'
+ taskExecuteType: BATCH
+ - id: 2
+ name: B
+ taskType: LogicFakeTask
+ workflowInstanceId: 1
+ workflowInstanceName:
workflow_with_forced_success_predecessor-20240816071251690
+ projectCode: 1
+ taskCode: 2
+ taskDefinitionVersion: 1
+ state: FAILURE
+ firstSubmitTime: 2024-08-16 07:12:54
+ submitTime: 2024-08-16 07:12:55
+ startTime: 2024-08-16 07:12:55
+ endTime: 2024-08-16 07:12:57
+ retryTimes: 0
+ host: 127.0.0.1:1234
+ maxRetryTimes: 0
+ taskParams: '{"localParams":null,"varPool":[],"shellScript":"echo hello"}'
+ flag: YES
+ retryInterval: 0
+ delayTime: 0
+ workerGroup: default
+ executorId: 1
+ varPool: '[]'
+ taskExecuteType: BATCH
+
+workflows:
+ - name: workflow_with_forced_success_predecessor
+ code: 1
+ version: 1
+ projectCode: 1
+ description: This is a fake workflow with a forced-success predecessor
+ releaseState: ONLINE
+ createTime: 2024-08-12 00:00:00
+ updateTime: 2021-08-12 00:00:00
+ userId: 1
+ executionType: PARALLEL
+
+tasks:
+ - name: A
+ code: 1
+ version: 1
+ projectCode: 1
+ userId: 1
+ taskType: LogicFakeTask
+ taskParams: '{"localParams":null,"varPool":[],"shellScript":"echo hello"}'
+ workerGroup: default
+ createTime: 2024-08-12 00:00:00
+ updateTime: 2021-08-12 00:00:00
+ taskExecuteType: BATCH
+ - name: B
+ code: 2
+ version: 1
+ projectCode: 1
+ userId: 1
+ taskType: LogicFakeTask
+ taskParams: '{"localParams":null,"varPool":[],"shellScript":"echo hello"}'
+ workerGroup: default
+ createTime: 2024-08-12 00:00:00
+ updateTime: 2021-08-12 00:00:00
+ taskExecuteType: BATCH
+
+taskRelations:
+ - projectCode: 1
+ workflowDefinitionCode: 1
+ workflowDefinitionVersion: 1
+ preTaskCode: 0
+ preTaskVersion: 0
+ postTaskCode: 1
+ postTaskVersion: 1
+ createTime: 2024-08-12 00:00:00
+ updateTime: 2024-08-12 00:00:00
+ - projectCode: 1
+ workflowDefinitionCode: 1
+ workflowDefinitionVersion: 1
+ preTaskCode: 1
+ preTaskVersion: 1
+ postTaskCode: 2
+ postTaskVersion: 1
+ createTime: 2024-08-12 00:00:00
+ updateTime: 2024-08-12 00:00:00