apache/dolphinscheduler · error · UnsupportedOperationException

updateWorkflowInstance + workflowInstanceId + state failed,

Error message

updateWorkflowInstance + workflowInstanceId + state failed, expect original state is + originalStatus.name() + actual state is : {} + workflowInstance.getState().name()

What it means

updateWorkflowInstanceState uses optimistic-style state transition: the UPDATE only succeeds when the row's current state equals the expected originalStatus. If update count != 1 but the instance exists, its stored state differs from what the caller expected, and UnsupportedOperationException is thrown with both expected and actual states.

Source

Thrown at dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/repository/impl/WorkflowInstanceDaoImpl.java:76

    public void upsertWorkflowInstance(@NonNull WorkflowInstance workflowInstance) {
        if (workflowInstance.getId() != null) {
            updateById(workflowInstance);
        } else {
            insert(workflowInstance);
        }
    }

    @Override
    public void updateWorkflowInstanceState(Integer workflowInstanceId, WorkflowExecutionStatus originalStatus,
                                            WorkflowExecutionStatus targetStatus) {
        int update = mybatisMapper.updateWorkflowInstanceState(workflowInstanceId, originalStatus, targetStatus);
        if (update != 1) {
            WorkflowInstance workflowInstance = mybatisMapper.selectById(workflowInstanceId);
            if (workflowInstance == null) {
                throw new UnsupportedOperationException("updateWorkflowInstance " + workflowInstanceId
                        + " state failed, the workflow instance is not exist in db");
            }
            throw new UnsupportedOperationException(
                    "updateWorkflowInstance " + workflowInstanceId + " state failed, expect original state is "
                            + originalStatus.name() + " actual state is : {} " + workflowInstance.getState().name());
        }
    }

    @Override
    public void forceUpdateWorkflowInstanceState(Integer id, WorkflowExecutionStatus status) {
        mybatisMapper.forceUpdateWorkflowInstanceState(id, status);
    }

    /**
     * find last scheduler process instance in the date interval
     *
     * @param workflowDefinitionCode definitionCode
     * @param taskDefinitionCode    definitionCode
     * @param dateInterval          dateInterval
     * @return process instance
     */

View on GitHub (pinned to 02eac45a1b)

Solutions

  1. Re-read the instance's current state and recompute the transition from the actual state.
  2. Serialize state changes (single actor per instance) or tolerate already-transitioned states instead of throwing.
  3. Retry the whole read-modify-write cycle when a concurrent transition is detected.
  4. Log expected vs actual state and treat it as a benign conflict where the business logic allows.

Example fix

// before
workflowInstanceDao.updateWorkflowInstanceState(id, WorkflowExecutionStatus.RUNNING, WorkflowExecutionStatus.STOP);
// after
WorkflowInstance wi = workflowInstanceDao.queryById(id);
if (wi != null && wi.getState() == WorkflowExecutionStatus.RUNNING) {
    workflowInstanceDao.updateWorkflowInstanceState(id, wi.getState(), WorkflowExecutionStatus.STOP);
} else {
    log.warn("instance {} already in state {}, skip stop", id, wi == null ? null : wi.getState());
}
Defensive patterns

Strategy: retry

Validate before calling

WorkflowInstance wi = workflowInstanceDao.queryById(id);
if (wi == null || wi.getState() != expectedOriginal) {
    log.warn("instance {} state is {}, not {}; skip transition", id, wi == null ? null : wi.getState(), expectedOriginal);
    return;
}

Try / catch

try {
    workflowInstanceDao.updateWorkflowInstanceState(id, original, target);
} catch (UnsupportedOperationException e) {
    // re-read state and retry the transition from actual state
    WorkflowInstance wi = workflowInstanceDao.queryById(id);
    if (wi != null) {
        workflowInstanceDao.updateWorkflowInstanceState(id, wi.getState(), target);
    }
}

Prevention

When it happens

Trigger: Two threads/processes (e.g. master failover plus a kill command, or a retry) change the instance state concurrently so the expected originalStatus no longer matches; calling with a wrong originalStatus for the instance's current phase.

Common situations: Master/worker race conditions during failover; submitting state transitions twice (double kill/restart); code assuming READY_PAUSE/RUNNING when the instance already moved to STOP/FAILURE.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of apache/dolphinscheduler@02eac45a1b (2026-09-06). Data as JSON: /api/errors/e9dbd94bce85052a. Report an issue: GitHub.