Repository navigation
[Fix-18543][DAO] Return only the valid task instance per task code - #18544
Conversation
- Resolve the last attempt by max(id) so tied end_time no longer duplicates rows - Keep excluding attempts which have not finished yet - Add a TaskInstanceDaoImplTest case for attempts sharing an end_time Closes apache#18543 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
SbloodyS
left a comment
There was a problem hiding this comment.
In the current execution logic, there is only one running task instance under the same task code in the same workflow instance. We should find out why there is dirty data, rather than avoid this problem.
|
Thanks for the review. I agree there should only ever be one running task instance per task code, but the rows this query returns are not running instances, and the duplicates are not dirty data. The query only looks at finished attempts. // DependentExecute#calculateResultForTasks
if (workflowInstance.getState().isSuccess()) {
...
taskInstanceDao.queryLastTaskInstanceListIntervalInWorkflowInstance(workflowInstance.getId(), ...)So there is no running instance involved in this path at all. Multiple finished rows per task code are created by the engine on purpose. Retry, failover and failed-recover each // RetryTaskInstanceFactory#createTaskInstance
final TaskInstance taskInstance = cloneTaskInstance(needRetryTaskInstance);
taskInstance.setId(null);
...
taskInstanceDao.insert(taskInstance);
needRetryTaskInstance.setFlag(Flag.NO);
taskInstanceDao.updateById(needRetryTaskInstance);
The defect is the tie-break, not the data. The query groups by on instance.workflow_instance_id = t_max.workflow_instance_id
and instance.task_code = t_max.task_code
and instance.end_time = t_max.max_end_time
`end_time` datetime DEFAULT NULL COMMENT 'task end time',So a short task that fails and is retried (or failed over) inside the same second produces two attempts with byte-identical .collect(Collectors.toMap(TaskInstance::getTaskCode, TaskInstance::getState));
The singular sibling in the same mapper already handles this correctly and can't return duplicates: <select id="findLastTaskInstance" ...>
... order by end_time desc limit 1The plural version is simply inconsistent with it. Why If you'd rather keep The added test reproduces it: it fails on |
As I said before, this is not the root cause. If there is no dirty data, this problem will not appear. |
|
Do all instances of the same task have end times within the same second? |
…ne writes Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
No, and they don't need to. Two attempts of one task code ending in the same second is enough to .collect(Collectors.toMap(TaskInstance::getTaskCode, TaskInstance::getState));A single duplicated key throws, so one tied pair among a hundred well-spaced tasks fails the When two attempts do tie:
To be straight about the exposure: on PostgreSQL |
I think we're disagreeing about which data counts as dirty, so I made the reproduction match the // A failed attempt and the retry which replaced it, both finishing within the same second. On MySQL
// t_ds_task_instance.end_time is a datetime without fractional seconds, so the two attempts end up
// with exactly the same end_time. This is exactly the state RetryTaskInstanceFactory leaves behind:
// the superseded attempt is flagged invalid and the new attempt is the only valid one.
insertTaskInstance(EXTRACT_TASK, TaskExecutionStatus.FAILURE, sameEndTime, Flag.NO);
insertTaskInstance(EXTRACT_TASK, TaskExecutionStatus.SUCCESS, sameEndTime, Flag.YES);There is now exactly one valid task instance for that task code, which is the invariant you're No row in that fixture is dirty. Both are written by the engine on purpose, the superseded one is If your point is that the query should be honouring the -- (a) current PR: deterministic newest attempt
select max(id) as max_id from t_ds_task_instance ... group by task_code-- (b) rely on the engine invariant instead
... and flag = 1I chose (a) because it holds even if the invariant is ever violated, and because the singular sibling And if you do think a second finished row for one task code shouldn't exist at all, could you point |
|
|
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
@SbloodyS Thanks, that definition helps, and I've now made the query follow it (below). On the fixture first: it contains one Steps to reproduceMetadata DB must be MySQL (see the note at the end for why).
Being straight about it: step 3 is timing dependent. If the two attempts happen to straddle a second I also want to be upfront that I found this by reading the query rather than from a production What I changedSince select max(id) as max_id
from t_ds_task_instance
where 1=1
and workflow_instance_id = #{workflowInstanceId}
and state != 8
and flag = 1
and end_time is not null
...
group by task_code
If you would rather have only |
|
Thanks @xiangzihao for merging
One job is red, but it never reached a test: That is Maven Central dropping the TLS handshake while resolving a surefire plugin POM, in a module this PR does not touch. Because the matrix is fail-fast it then cancelled ten sibling jobs. Could someone re-run the failed jobs when convenient? I don't have permission to do it myself. @SbloodyS, whenever you have a moment: the query now follows the definition you gave. It filters |
SbloodyS
left a comment
There was a problem hiding this comment.
Adding flag = 1 fixes the reported path: retry, failover, and recovery intentionally retain the previous attempt as flag = NO, and the old query incorrectly allowed that historical row to match the same max(end_time) as the valid attempt.
However, max(id) adds a separate fallback that silently selects one row if multiple flag = YES instances exist. According to the valid-instance invariant discussed above, that state is invalid and should remain observable instead of being hidden. It also changes the existing ordering semantics from the greatest end_time to the greatest insertion ID.
Please scope the fix to selecting only the valid, finished instance, or retain the existing end_time ordering while applying flag = 1 to both the grouped rows and the outer matched row. If ordering by ID is intentional, please add a test where ID order and end_time order differ and document the semantic change.
…h sides of the join
|
Thanks @SbloodyS. I went with the second option and removed The query is now the one on
The join is back on The regression test is unchanged: a |
I still cannot understand, why two task instances belong to the same task code can have the same end time at one workflow instance, could this really happen? |
|
@ruanwenjun Thanks for the review. Yes, it can happen, although it is narrow. It needs two conditions together:
The step-by-step reproduction is in this comment. It is timing dependent, which is why the test builds the stored rows directly. The fix does not depend on how often this happens, though. The |
Was this PR generated or assisted by AI?
YES. The bug was found, and the fix and the test were written with AI assistance
(Claude Code). I reviewed the change at a high level and verified it via the added test
and the module test suite.
Purpose of the pull request
Closes #18543.
findLastTaskInstancesresolves the last task instance per task code by grouping ontask_codewithmax(end_time)and joining back oninstance.end_time = t_max.max_end_time.Neither side filters on
flag.Retry, failover and failed-recover keep the previous attempt as a historical row with
flag = NOand insert the new attempt withflag = YES. When both attempts carry the sameend_time, the historical row also matchesmax(end_time)and the query returns two rowsfor one task code. On MySQL
t_ds_task_instance.end_timeis adatetimewithoutfractional seconds, so a task which fails and is retried quickly can easily produce this.
The only caller,
DependentExecute#dependResultByAllTaskOfWorkflowInstance, keys theresult by task code with
Collectors.toMapand no merge function, so the duplicate throwsIllegalStateException: Duplicate keyand the dependency evaluation of a DEPENDENT taskconfigured with "ALL tasks" fails.
Brief change log
TaskInstanceMapper.xml#findLastTaskInstances: only consider the valid instance of eachtask code, by applying
flag = 1both to the grouped rows and to the outer matched row.The ordering is unchanged: the last instance is still the one with the greatest
end_time.TaskInstanceDaoImplTestcase covering a historical attempt and its valid retrysharing an
end_time.Verify this pull request
This change added tests and can be verified as follows:
TaskInstanceDaoImplTest#queryLastTaskInstanceListIntervalInWorkflowInstanceWhenAttemptsShareTheSameEndTime,which inserts a
FAILUREattempt withflag = NOand itsSUCCESSretry withflag = YES, both with the sameend_time, and asserts that only the valid attempt isreturned.
./mvnw -pl dolphinscheduler-dao -am clean test \ -Dtest=TaskInstanceDaoImplTest \ -Dsurefire.failIfNoSpecifiedTests=falseVerified locally:
flag = 1conditions withexpected: <1> but was: <2>and passes with this change.
TaskInstanceDaoImplTestcases still pass.dolphinscheduler-daosuite passes: 284 tests, 0 failures, 0 errors../mvnw -pl dolphinscheduler-dao spotless:checkpasses.Pull Request Notice
Pull Request Notice
If your pull request contains incompatible change, you should also add it to
docs/docs/en/guide/upgrade/incompatible.md🤖 Generated with Claude Code