{"record":{"id":"5361d38382fac3df","repo":"libnyanpasu/clash-nyanpasu","slug":"cannot-promote-a-compensating-materialization","errorCode":null,"errorMessage":"cannot promote a compensating materialization","messagePattern":"cannot promote a compensating materialization","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"backend/tauri/src/service/profile_file.rs","lineNumber":1640,"sourceCode":"    }\n\n    fn promote(&self, prepared: &PreparedMaterialization) -> anyhow::Result<()> {\n        let root = self.ensure_materialization_layout()?;\n        let operation_id = prepared.operation_id();\n        let Some((mut location, journal)) = self.locate_materialization(&root, operation_id)?\n        else {\n            bail!(\"materialization journal not found for operation {operation_id}\");\n        };\n\n        if let Some(promoting) = location.promoting() {\n            Self::transition_journal(&root, operation_id, location, promoting)?;\n            location = promoting;\n        }\n        if matches!(\n            location,\n            JournalLocation::StateCompensating | JournalLocation::FileCompensating\n        ) {\n            bail!(\"cannot promote a compensating materialization\");\n        }\n\n        let target = self.resolve(&journal.managed_path)?;\n        if Self::path_hash(&target)? != journal.hash {\n            self.promote_resource(&root, operation_id, &target, &journal.hash)?;\n        }\n        if location == JournalLocation::FilePromoting {\n            Self::transition_journal(\n                &root,\n                operation_id,\n                JournalLocation::FilePromoting,\n                JournalLocation::FilePromoted,\n            )?;\n        }\n        Ok(())\n    }\n\n    fn complete(&self, prepared: &PreparedMaterialization) -> anyhow::Result<()> {","sourceCodeStart":1622,"sourceCodeEnd":1658,"githubUrl":"https://github.com/libnyanpasu/clash-nyanpasu/blob/f7dbce2997c633e484f54788035e770b3ee99773/backend/tauri/src/service/profile_file.rs#L1622-L1658","documentation":"Promote was called on a materialization whose journal is in a compensating phase (StateCompensating or FileCompensating). Compensating means a prior step failed and the operation is being rolled back; promoting a rollback in progress would violate the two-phase protocol, so the service refuses.","triggerScenarios":"Calling promote(prepared) after compensate(prepared) was initiated (journal transitioned to StateCompensating/FileCompensating), or promoting a handle recovered from disk whose journal shows a compensating phase written by rollback/recovery logic after a failed promote or complete.","commonSituations":"Application error-handling that calls compensate() on failure but then retries promote() with the same handle instead of re-preparing; crash-recovery code resuming operations whose journals recorded a compensating state; interleaved promote/compensate calls from concurrent tasks on the same operation.","solutions":["Do not reuse a compensating handle: run compensate() to finish the rollback, then call prepare_state_first/prepare_file_first to start a fresh operation.","Fix control flow so a failed operation is compensated once and then re-prepared, never re-promoted.","Serialize operations per profile path so concurrent promote/compensate on the same operation cannot interleave.","Check the journal phase (locate_materialization) before deciding to promote, and route compensating phases to the compensate path."],"exampleFix":"// before\nmatch op.run() {\n    Err(_) => service.compensate(&prepared)?,\n}\nservice.promote(&prepared)?; // wrong: promoting a compensating op\n\n// after\nmatch op.run() {\n    Err(_) => {\n        service.compensate(&prepared)?;\n        let prepared = service.prepare_state_first(&path, resource, revision)?;\n    }\n    _ => {}\n}\nservice.promote(&prepared)?;","handlingStrategy":"validation","validationCode":"if let Ok(Some((location, _journal))) = service.locate_materialization(&root, prepared.operation_id()) {\n    anyhow::ensure!(\n        !matches!(location, JournalLocation::StateCompensating | JournalLocation::FileCompensating),\n        \"operation {} is compensating; must re-prepare before promoting\",\n        prepared.operation_id()\n    );\n}","typeGuard":"fn is_compensating(loc: &JournalLocation) -> bool {\n    matches!(loc, JournalLocation::StateCompensating | JournalLocation::FileCompensating)\n}","tryCatchPattern":"if let Err(e) = service.promote(&prepared) {\n    if e.to_string().contains(\"compensating\") {\n        service.compensate(&prepared)?;\n        let prepared = service.prepare_state_first(&path, resource, revision)?;\n        service.promote(&prepared)?;\n    } else { return Err(e); }\n}","preventionTips":["After compensate(), always re-prepare; never re-promote the same handle.","Route operations through a single state machine: prepare -> promote -> complete, or prepare -> compensate.","Serialize materialization operations per managed path to avoid interleaved promote/compensate.","In crash-recovery code, inspect the journal phase and dispatch to the matching transition, never blindly promote."],"tags":["state-machine","two-phase-commit","invalid-state-transition","materialization"],"backgroundTag":"invalid-state-transition","analyzedSha":"f7dbce2997c633e484f54788035e770b3ee99773","analyzedAt":"2026-09-08T01:24:59.197Z","contentChangedAt":"2026-09-08T01:24:59.197Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}