Skip to content

[Bug] [DAO] findLastTaskInstances returns duplicated task instances and breaks the dependent task #18543

Description

@SEPURI-SAI-KRISHNA

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_time

end_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

  1. 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).
  2. Workflow B contains a DEPENDENT task depending on workflow A with
    "ALL tasks" selected (DEPENDENT_ALL_TASK_CODE).
  3. Run A to completion, then run B.
  4. B's dependent check fails with IllegalStateException: Duplicate key instead 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

Activity

  1. SbloodyS commented on Aug 11, 2026

    @SbloodyS
    Member

    Replied in #18544 (review)

  2. Adarsh-Me commented on Sep 1, 2026

    @Adarsh-Me
    Contributor

    Hi! I'd like to work on this. The join in TaskInstanceMapper.xml#findLastTaskInstances can 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!

  3. SEPURI-SAI-KRISHNA commented on Sep 1, 2026

    @SEPURI-SAI-KRISHNA
    ContributorAuthor

    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_code and selects max(id), and the outer query joins on instance.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 filters flag = 1 so only the valid instance of each task code is considered, and it adds a mapper-level regression test in TaskInstanceDaoImplTest that reproduces two attempts sharing an end_time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdiscussiondiscussion

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions