{"record":{"id":"194e256cf52c38e9","repo":"sgl-project/sglang","slug":"cannot-transition-request-id-from-terminal-state","errorCode":null,"errorMessage":"Cannot transition {request_id} from terminal state {old_state.value} to {new_state.value}","messagePattern":"Cannot transition (.+?) from terminal state (.+?) to (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"warning","filePath":"python/sglang/multimodal_gen/runtime/disaggregation/request_state.py","lineNumber":111,"sourceCode":"        request_id: str,\n        new_state: RequestState,\n        *,\n        error: str | None = None,\n        encoder_instance: int | None = None,\n        denoiser_instance: int | None = None,\n        decoder_instance: int | None = None,\n    ) -> RequestRecord:\n        with self._lock:\n            record = self._requests.get(request_id)\n            if record is None:\n                raise ValueError(f\"Unknown request_id: {request_id}\")\n\n            old_state = record.state\n\n            if new_state in _TERMINAL_STATES and new_state != RequestState.DONE:\n                # FAILED / TIMED_OUT: allowed from any active state\n                if old_state not in _ACTIVE_STATES:\n                    raise ValueError(\n                        f\"Cannot transition {request_id} from terminal state \"\n                        f\"{old_state.value} to {new_state.value}\"\n                    )\n            elif new_state not in _VALID_TRANSITIONS.get(old_state, set()):\n                raise ValueError(\n                    f\"Invalid transition for {request_id}: \"\n                    f\"{old_state.value} -> {new_state.value}\"\n                )\n\n            record.state = new_state\n            record.last_transition_time = time.monotonic()\n            if error is not None:\n                record.error = error\n            if encoder_instance is not None:\n                record.encoder_instance = encoder_instance\n            if denoiser_instance is not None:\n                record.denoiser_instance = denoiser_instance\n            if decoder_instance is not None:","sourceCodeStart":93,"sourceCodeEnd":129,"githubUrl":"https://github.com/sgl-project/sglang/blob/0132848349585cfe6aae51c4941cbae872505f8a/python/sglang/multimodal_gen/runtime/disaggregation/request_state.py#L93-L129","documentation":"Raised by RequestStateStore.transition when moving a request that is already in a terminal state (FAILED/TIMED_OUT, or any non-active state) into another terminal state. Terminal states are final; the state machine only allows FAILED/TIMED_OUT transitions from active states, so re-failing or transitioning a finished request is rejected.","triggerScenarios":"Calling transition(id, FAILED or TIMED_OUT) when the record's current state is already terminal — e.g. two components race to report failure (timeout watchdog fires after _complete_with_error already marked the request FAILED), or a duplicate error frame is processed.","commonSituations":"Concurrent error paths (watchdog timeout + decoder error), duplicate error frames from retries, or handlers not checking the record state before completing with an error.","solutions":["Make error completion idempotent: check record.state is active (or catch this ValueError) before applying FAILED/TIMED_OUT","Serialize completions per request (e.g. via the store lock or a per-request completion flag) so only the first terminal transition wins","Cancel the watchdog/timeout task once a request reaches a terminal state","Log and drop duplicate terminal transitions at debug level instead of letting them propagate"],"exampleFix":"# before\nstore.transition(request_id, RequestState.FAILED)  # may race -> ValueError\n\n# after\nrecord = store.get(request_id)\nif record is None or record.state in _TERMINAL_STATES:\n    return  # already finished; idempotent no-op\ntry:\n    store.transition(request_id, RequestState.FAILED)\nexcept ValueError:\n    logger.debug(\"request %s already terminal\", request_id)","handlingStrategy":"try-catch","validationCode":"from sglang.multimodal_gen.runtime.disaggregation.request_state import _TERMINAL_STATES, _ACTIVE_STATES\nrecord = store.get(request_id)\nif record is not None and record.state not in _ACTIVE_STATES:\n    logger.debug(\"%s already terminal (%s); skipping error completion\", request_id, record.state)\n    return","typeGuard":null,"tryCatchPattern":"try:\n    store.transition(request_id, RequestState.FAILED)\nexcept ValueError as e:\n    if \"terminal state\" in str(e):\n        logger.debug(\"duplicate terminal transition for %s ignored\", request_id)\n        return\n    raise","preventionTips":["Make error completion idempotent — only the first terminal transition should win","Cancel watchdog/timeout timers once a request reaches a terminal state","Check record.state before calling transition with FAILED/TIMED_OUT"],"tags":["disaggregation","request-state","state-machine","terminal-state","race-condition"],"backgroundTag":"invalid-state-transition","analyzedSha":"0132848349585cfe6aae51c4941cbae872505f8a","analyzedAt":"2026-08-28T05:10:05.995Z","schemaVersion":2},"datasetVersion":"2026-08-28T06:17:29.519Z"}