|
17 | 17 |
|
18 | 18 | package org.apache.dolphinscheduler.api.executor.workflow; |
19 | 19 |
|
| 20 | +import static org.mockito.ArgumentMatchers.any; |
20 | 21 | import static org.mockito.ArgumentMatchers.anyLong; |
21 | 22 | import static org.mockito.Mockito.doReturn; |
22 | 23 | import static org.mockito.Mockito.never; |
| 24 | +import static org.mockito.Mockito.times; |
23 | 25 | import static org.mockito.Mockito.verify; |
24 | 26 | import static org.mockito.Mockito.when; |
25 | 27 |
|
|
41 | 43 | import java.util.List; |
42 | 44 | import java.util.Optional; |
43 | 45 | import java.util.Set; |
| 46 | +import java.util.stream.Collectors; |
44 | 47 |
|
45 | 48 | import org.junit.jupiter.api.Assertions; |
46 | 49 | import org.junit.jupiter.api.Test; |
@@ -146,7 +149,8 @@ public void testDoBackfillDependentWorkflow_WithDownstream_AllLevelDependent() t |
146 | 149 | .thenReturn(Optional.of(downstreamWorkflow)); |
147 | 150 |
|
148 | 151 | ArgumentCaptor<BackfillWorkflowDTO> captor = ArgumentCaptor.forClass(BackfillWorkflowDTO.class); |
149 | | - doReturn(Collections.singletonList(1)).when(backfillWorkflowExecutorDelegate).execute(captor.capture()); |
| 152 | + doReturn(Collections.singletonList(1)).when(backfillWorkflowExecutorDelegate) |
| 153 | + .executeWithVisitedCodes(captor.capture(), any()); |
150 | 154 |
|
151 | 155 | Method method = BackfillWorkflowExecutorDelegate.class.getDeclaredMethod( |
152 | 156 | "doBackfillDependentWorkflow", BackfillWorkflowDTO.class, List.class, Set.class); |
@@ -219,7 +223,8 @@ public void testDoBackfillDependentWorkflow_WithDownstream_SingleLevelDependent( |
219 | 223 | .thenReturn(Optional.of(downstreamWorkflow)); |
220 | 224 |
|
221 | 225 | ArgumentCaptor<BackfillWorkflowDTO> captor = ArgumentCaptor.forClass(BackfillWorkflowDTO.class); |
222 | | - doReturn(Collections.singletonList(1)).when(backfillWorkflowExecutorDelegate).execute(captor.capture()); |
| 226 | + doReturn(Collections.singletonList(1)).when(backfillWorkflowExecutorDelegate) |
| 227 | + .executeWithVisitedCodes(captor.capture(), any()); |
223 | 228 |
|
224 | 229 | Method method = BackfillWorkflowExecutorDelegate.class.getDeclaredMethod( |
225 | 230 | "doBackfillDependentWorkflow", BackfillWorkflowDTO.class, List.class, Set.class); |
@@ -281,7 +286,7 @@ public void testDoBackfillDependentWorkflow_SkipWorkflowNotFound() throws Except |
281 | 286 | method.invoke(backfillWorkflowExecutorDelegate, dto, Collections.singletonList("2026-02-01 00:00:00"), |
282 | 287 | visitedCodes); |
283 | 288 |
|
284 | | - verify(backfillWorkflowExecutorDelegate, never()).execute(org.mockito.ArgumentMatchers.any()); |
| 289 | + verify(backfillWorkflowExecutorDelegate, never()).executeWithVisitedCodes(any(), any()); |
285 | 290 | } |
286 | 291 |
|
287 | 292 | @Test |
@@ -324,6 +329,78 @@ public void testDoBackfillDependentWorkflow_SkipOfflineWorkflow() throws Excepti |
324 | 329 | method.invoke(backfillWorkflowExecutorDelegate, dto, Collections.singletonList("2026-02-01 00:00:00"), |
325 | 330 | visitedCodes); |
326 | 331 |
|
327 | | - verify(backfillWorkflowExecutorDelegate, never()).execute(org.mockito.ArgumentMatchers.any()); |
| 332 | + verify(backfillWorkflowExecutorDelegate, never()).executeWithVisitedCodes(any(), any()); |
| 333 | + } |
| 334 | + |
| 335 | + @Test |
| 336 | + public void testDoBackfillDependentWorkflow_MultiLevelAndCycle() throws Exception { |
| 337 | + long workflowA = 10L; |
| 338 | + long workflowB = 20L; |
| 339 | + long workflowC = 30L; |
| 340 | + |
| 341 | + WorkflowDefinition upstreamA = |
| 342 | + WorkflowDefinition.builder().code(workflowA).releaseState(ReleaseState.ONLINE).build(); |
| 343 | + WorkflowDefinition downstreamB = |
| 344 | + WorkflowDefinition.builder().code(workflowB).releaseState(ReleaseState.ONLINE).warningGroupId(1) |
| 345 | + .build(); |
| 346 | + WorkflowDefinition downstreamC = |
| 347 | + WorkflowDefinition.builder().code(workflowC).releaseState(ReleaseState.ONLINE).warningGroupId(2) |
| 348 | + .build(); |
| 349 | + |
| 350 | + BackfillWorkflowDTO.BackfillParamsDTO params = BackfillWorkflowDTO.BackfillParamsDTO.builder() |
| 351 | + .runMode(RunMode.RUN_MODE_SERIAL) |
| 352 | + .backfillDateList(Collections.<ZonedDateTime>emptyList()) |
| 353 | + .backfillDependentMode(ComplementDependentMode.ALL_DEPENDENT) |
| 354 | + .allLevelDependent(true) |
| 355 | + .executionOrder(ExecutionOrder.ASC_ORDER) |
| 356 | + .build(); |
| 357 | + |
| 358 | + BackfillWorkflowDTO dtoA = BackfillWorkflowDTO.builder() |
| 359 | + .workflowDefinition(upstreamA) |
| 360 | + .backfillParams(params) |
| 361 | + .build(); |
| 362 | + |
| 363 | + DependentWorkflowDefinition depToB = new DependentWorkflowDefinition(); |
| 364 | + depToB.setWorkflowDefinitionCode(workflowB); |
| 365 | + DependentWorkflowDefinition depToA = new DependentWorkflowDefinition(); |
| 366 | + depToA.setWorkflowDefinitionCode(workflowA); |
| 367 | + DependentWorkflowDefinition depToC = new DependentWorkflowDefinition(); |
| 368 | + depToC.setWorkflowDefinitionCode(workflowC); |
| 369 | + |
| 370 | + when(workflowLineageService.queryDownstreamDependentWorkflowDefinitions(workflowA)) |
| 371 | + .thenReturn(Collections.singletonList(depToB)); |
| 372 | + when(workflowLineageService.queryDownstreamDependentWorkflowDefinitions(workflowB)) |
| 373 | + .thenReturn(Arrays.asList(depToA, depToC)); |
| 374 | + when(workflowDefinitionDao.queryByCode(workflowB)).thenReturn(Optional.of(downstreamB)); |
| 375 | + when(workflowDefinitionDao.queryByCode(workflowC)).thenReturn(Optional.of(downstreamC)); |
| 376 | + |
| 377 | + ArgumentCaptor<BackfillWorkflowDTO> captor = ArgumentCaptor.forClass(BackfillWorkflowDTO.class); |
| 378 | + doReturn(Collections.singletonList(1)).when(backfillWorkflowExecutorDelegate) |
| 379 | + .executeWithVisitedCodes(captor.capture(), any()); |
| 380 | + |
| 381 | + Method method = BackfillWorkflowExecutorDelegate.class.getDeclaredMethod( |
| 382 | + "doBackfillDependentWorkflow", BackfillWorkflowDTO.class, List.class, Set.class); |
| 383 | + method.setAccessible(true); |
| 384 | + |
| 385 | + List<String> backfillTimeList = Collections.singletonList("2026-02-01 00:00:00"); |
| 386 | + Set<Long> visitedCodes = new HashSet<>(); |
| 387 | + visitedCodes.add(workflowA); |
| 388 | + |
| 389 | + // Level 1: A -> B |
| 390 | + method.invoke(backfillWorkflowExecutorDelegate, dtoA, backfillTimeList, visitedCodes); |
| 391 | + BackfillWorkflowDTO dtoB = captor.getAllValues().get(0); |
| 392 | + |
| 393 | + // Level 2: B -> A(cycle, should skip) and B -> C(should trigger) |
| 394 | + method.invoke(backfillWorkflowExecutorDelegate, dtoB, backfillTimeList, visitedCodes); |
| 395 | + |
| 396 | + verify(backfillWorkflowExecutorDelegate, times(2)).executeWithVisitedCodes(any(), any()); |
| 397 | + verify(workflowDefinitionDao, never()).queryByCode(workflowA); |
| 398 | + |
| 399 | + List<Long> triggeredCodes = captor.getAllValues().stream() |
| 400 | + .map(it -> it.getWorkflowDefinition().getCode()) |
| 401 | + .collect(Collectors.toList()); |
| 402 | + Assertions.assertEquals(Arrays.asList(workflowB, workflowC), triggeredCodes); |
| 403 | + Assertions.assertTrue(visitedCodes.contains(workflowB)); |
| 404 | + Assertions.assertTrue(visitedCodes.contains(workflowC)); |
328 | 405 | } |
329 | 406 | } |
0 commit comments