Search before asking
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
- 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 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?
Code of Conduct
Search before asking
What happened
TaskInstanceMapper#findLastTaskInstancesis supposed to return the last task instanceper 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_timeis not unique. When two attempts of the same task share the very sameend_time, both of them matchinstance.end_time = t_max.max_end_timeand the queryreturns two rows for one task code.
This is easy to hit on MySQL, where
t_ds_task_instance.end_timeis declared asa
datetimewithout fractional seconds, so every timestamp is truncated to a wholesecond. 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 onstate != 8only — it does not filter onflag, so the invalidated previous attempts are included as well.The only caller is
DependentExecute#dependResultByAllTaskOfWorkflowInstance, which keysthe result by task code:
Collectors.toMaphas no merge function, so a duplicated task code throwsand the dependency evaluation of the DEPENDENT task blows up.
What you expected to happen
findLastTaskInstancesreturns at most one task instance per task code — the lastattempt — so a DEPENDENT task can evaluate the upstream workflow normally.
How to reproduce
failed-retry interval so that both attempts finish within the same second (on MySQL
any two attempts finishing in the same second are enough).
"ALL tasks" selected (
DEPENDENT_ALL_TASK_CODE).IllegalStateException: Duplicate keyinstead ofresolving the dependency.
It also reproduces directly at the DAO level — insert two task instances with the same
task_codeand the sameend_timeinto one workflow instance and callqueryLastTaskInstanceListIntervalInWorkflowInstance; 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?
Code of Conduct