{"record":{"id":"18d623afb5ba3a4f","repo":"libnyanpasu/clash-nyanpasu","slug":"cannot-cancel-an-activated-cleanup-operation","errorCode":null,"errorMessage":"cannot cancel an activated cleanup operation","messagePattern":"cannot cancel an activated cleanup operation","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"backend/tauri/src/service/profile_file.rs","lineNumber":1768,"sourceCode":"            operation_id,\n            CleanupPhase::Pending,\n            CleanupPhase::Ready,\n        )\n        .with_context(|| format!(\"activate cleanup operation {operation_id}\"))\n    }\n\n    fn cancel_cleanup(&self, cleanup: &PreparedCleanup) -> anyhow::Result<()> {\n        let root = self.ensure_materialization_layout()?;\n        let operation_id = cleanup.operation_id();\n        match Self::locate_cleanup(&root, operation_id)? {\n            None => Ok(()),\n            Some((CleanupPhase::Pending, _)) => Self::remove_private_regular(&Self::cleanup_path(\n                &root,\n                CleanupPhase::Pending,\n                operation_id,\n            )),\n            Some((CleanupPhase::Ready, _)) => {\n                bail!(\"cannot cancel an activated cleanup operation\")\n            }\n        }\n    }\n\n    fn retry_cleanup(\n        &self,\n        cleanup: &PreparedCleanup,\n        profiles: &Profiles,\n    ) -> anyhow::Result<CleanupOutcome> {\n        let root = self.ensure_materialization_layout()?;\n        let operation_id = cleanup.operation_id();\n        let Some((phase, journal)) = Self::locate_cleanup(&root, operation_id)? else {\n            return Ok(CleanupOutcome::AlreadyAbsent);\n        };\n        if phase == CleanupPhase::Pending {\n            self.activate_cleanup(cleanup)?;\n        }\n","sourceCodeStart":1750,"sourceCodeEnd":1786,"githubUrl":"https://github.com/libnyanpasu/clash-nyanpasu/blob/f7dbce2997c633e484f54788035e770b3ee99773/backend/tauri/src/service/profile_file.rs#L1750-L1786","documentation":"Cleanup operations in the profile-file transaction framework go through phases (Pending, then Ready once activated/armed). Cancelling is only allowed while the operation is still Pending; once the cleanup has reached the Ready phase it is considered activated and must run (or be retried/compensated), so cancel_cleanup bails with this error to avoid un-arming a cleanup that may already be partially executed.","triggerScenarios":"Calling cancel_cleanup (the message-based cancel path that dispatches on the (CleanupPhase, _) pair) for an operation whose journal records CleanupPhase::Ready. The match arm Some((CleanupPhase::Ready, _)) unconditionally bails.","commonSituations":"A race between the activation step (which promotes Pending -> Ready) and a user/tool issuing cancel; double-cancellation attempts after a retry already armed the cleanup; application shutdown while cleanup was armed and then a cancel is issued on restart.","solutions":["Do not cancel a Ready cleanup: instead let it execute, or use the compensation/retry path to run and clear it","Poll the journal state before cancelling: only issue cancel when the recorded phase is Pending","If the cleanup must not run, examine whether a rollback of the promotion (compensate) is the correct operation instead of cancel","Check for code paths that promote the phase too early (activation race) and serialize cancel against activation under the same lock/actor"],"exampleFix":"// before: unconditional cancel\nmanager.cancel_cleanup(operation_id)?;\n// after: only cancel while still pending\nlet phase = manager.cleanup_phase(operation_id)?;\nif phase == CleanupPhase::Pending {\n    manager.cancel_cleanup(operation_id)?;\n} else {\n    manager.compensate(operation_id)?;\n}","handlingStrategy":"validation","validationCode":"let phase = manager.cleanup_phase(operation_id)?;\nif phase != CleanupPhase::Pending {\n    // cannot cancel; route to compensation instead\n    return Err(anyhow::anyhow!(\"cleanup already activated\"));\n}\nmanager.cancel_cleanup(operation_id)?;","typeGuard":"fn is_cancellable(phase: CleanupPhase) -> bool {\n    matches!(phase, CleanupPhase::Pending)\n}","tryCatchPattern":"match manager.cancel_cleanup(op_id) {\n    Err(e) if e.to_string().contains(\"cannot cancel an activated cleanup\") => {\n        manager.compensate(op_id)?; // activated: compensate instead\n    }\n    other => other?,\n}","preventionTips":["Read the journal phase immediately before cancelling; cancel only Pending operations","Avoid issuing cancel concurrently with activation; route both through a single serialized owner (actor/lock)","Design UX so an activated cleanup is presented as 'run or compensate', not 'cancel'","On shutdown, decide cancel-vs-compensate based on phase before exiting"],"tags":["transaction","cleanup","lifecycle","invalid-state"],"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"}