Repository navigation
[Bug] [DAO] findLastTaskInstances returns duplicated task instances and breaks the dependent task #18543
Description
Search before asking
- I had searched in the issues and found no similar issues.
What happened
TaskInstanceMapper#findLastTaskInstances is supposed to return the last task instance
per task code of a workflow instance. It resolves "last" by joining on the maximum
end_time:
from t_ds_task_instance instance
join (
select task_code, max(end_time) as max_end_time, workflow_instance_id
from t_ds_task_instance
where 1=1
and workflow_instance_id = #{workflowInstanceId}
and state != 8
...
group by task_code
) t_max
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_timeend_time is not unique. When two attempts of the same task share the very same
end_time, both of them match instance.end_time = t_max.max_end_time and the query
returns two rows for one task code.
This is easy to hit on MySQL, where t_ds_task_instance.end_time is declared as
`end_time` datetime DEFAULT NULL COMMENT 'task end time',a datetime without fractional seconds, so every timestamp is truncated to a whole
second. A task that fails and is retried quickly (a short or zero failed-retry interval,
or simply a fast-failing task) ends up with the failed attempt and the retry sharing an
identical end_time. Note the query filters on state != 8 only — it does not filter on
flag, so the invalidated previous attempts are included as well.
The only caller is DependentExecute#dependResultByAllTaskOfWorkflowInstance, which keys
the result by task code:
Map<Long, TaskExecutionStatus> taskExecutionStatusMap =
taskInstanceList.stream()
.filter(taskInstance -> taskInstance.getTaskExecuteType() != TaskExecuteType.STREAM)
.collect(Collectors.toMap(TaskInstance::getTaskCode, TaskInstance::getState));Collectors.toMap has no merge function, so a duplicated task code throws
java.lang.IllegalStateException: Duplicate key <taskCode> (attempted merging values ... and ...)
and the dependency evaluation of the DEPENDENT task blows up.
What you expected to happen
findLastTaskInstances returns at most one task instance per task code — the last
attempt — so a DEPENDENT task can evaluate the upstream workflow normally.
How to reproduce
- Workflow A contains a task that fails and then succeeds on retry, with a short
failed-retry interval so that both attempts finish within the same second (on MySQL
any two attempts finishing in the same second are enough). - Workflow B contains a DEPENDENT task depending on workflow A with
"ALL tasks" selected (DEPENDENT_ALL_TASK_CODE). - Run A to completion, then run B.
- B's dependent check fails with
IllegalStateException: Duplicate keyinstead of
resolving the dependency.
It also reproduces directly at the DAO level — insert two task instances with the same
task_code and the same end_time into one workflow instance and call
queryLastTaskInstanceListIntervalInWorkflowInstance; it returns 2 rows instead of 1.
Anything else
The same duplication would silently affect any future caller of this query, since the
method name (findLastTaskInstances / queryLastTaskInstanceListIntervalInWorkflowInstance)
promises one row per task code.
Version
dev
Are you willing to submit PR?
- Yes I am willing to submit a PR!
Code of Conduct
- I agree to follow this project's Code of Conduct
Activity
SbloodyS commentedon Aug 11, 2026
Replied in #18544 (review)
Adarsh-Me commentedon Sep 1, 2026
Hi! I'd like to work on this. The join in
TaskInstanceMapper.xml#findLastTaskInstancescan produce duplicate rows when several instances of the same task_code share the same max(end_time). My plan: deduplicate by picking a deterministic row per task_code (e.g., also tie-break on id desc), verify against the dependent-task code path, and add a mapper-level regression test. Could you please assign this to me? Thanks!SEPURI-SAI-KRISHNA commentedon Sep 1, 2026
Hi @Adarsh-Me, thanks for the interest. This one is already covered: #18544 has been open since 10 Aug and is under review by @SbloodyS.
It takes the approach you describe. The subquery groups by
task_codeand selectsmax(id), and the outer query joins oninstance.id = t_max.max_id, so exactly one row comes back per task code by construction rather than by a tie-break after the fact. It also filtersflag = 1so only the valid instance of each task code is considered, and it adds a mapper-level regression test inTaskInstanceDaoImplTestthat reproduces two attempts sharing anend_time.