[Fix-18543][DAO] Return only the last task instance per task code - #18544
[Fix-18543][DAO] Return only the last task instance per task code#18544SEPURI-SAI-KRISHNA wants to merge 4 commits into
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 |
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 joining oninstance.end_time = t_max.max_end_time.end_timeis not unique, so when two attemptsof the same task share an
end_timethe query returns two rows for one task code.On MySQL
t_ds_task_instance.end_timeis adatetimewithout fractional seconds, soevery value is truncated to a whole second and a task which fails and is retried quickly
ends up with both attempts carrying an identical
end_time. The query filters onstate != 8only, not onflag, so the invalidated previous attempt is included too.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: resolve the last attempt per task codewith
max(id)and join on the primary key, which yields exactly one row per task code.end_time is not nullin the sub-query. Previously this was implicit —max(end_time)ignores NULLs and
instance.end_time = t_max.max_end_timenever matches a NULL.TaskInstanceDaoImplTestcase covering two attempts sharing anend_time.A note on
max(id)vsmax(end_time)t_ds_task_instance.idis an auto-increment primary key, so the highest id for a(
workflow_instance_id,task_code) pair is the most recently created attempt. A retryrow is only inserted after the previous attempt has ended, so for retries the latest id
and the latest
end_timeidentify the same row — but onlyidis unique, so onlyidcan guarantee a single row. This also removes the non-aggregated
workflow_instance_idfrom the
group by task_codesub-query, which is friendlier toONLY_FULL_GROUP_BYonMySQL.
Happy to switch to a different tie-break if maintainers prefer to keep
end_timeas theordering key.
Verify this pull request
This change added tests and can be verified as follows:
TaskInstanceDaoImplTest#queryLastTaskInstanceListIntervalInWorkflowInstanceWhenAttemptsShareTheSameEndTime,which inserts a
FAILUREattempt and itsSUCCESSretry with the sameend_timeandasserts a single row — the last attempt — is returned.
./mvnw -pl dolphinscheduler-dao -am clean test \ -Dtest=TaskInstanceDaoImplTest \ -Dsurefire.failIfNoSpecifiedTests=falseVerified locally:
devwithexpected: <1> but was: <2>and passes with thischange.
TaskInstanceDaoImplTestcases still pass, so the "last attempt"semantics are unchanged for the normal, non-tied case.
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