libnyanpasu/clash-nyanpasu · error

cannot cancel an activated cleanup operation

Error message

cannot cancel an activated cleanup operation

What it means

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.

Source

Thrown at backend/tauri/src/service/profile_file.rs:1768

            operation_id,
            CleanupPhase::Pending,
            CleanupPhase::Ready,
        )
        .with_context(|| format!("activate cleanup operation {operation_id}"))
    }

    fn cancel_cleanup(&self, cleanup: &PreparedCleanup) -> anyhow::Result<()> {
        let root = self.ensure_materialization_layout()?;
        let operation_id = cleanup.operation_id();
        match Self::locate_cleanup(&root, operation_id)? {
            None => Ok(()),
            Some((CleanupPhase::Pending, _)) => Self::remove_private_regular(&Self::cleanup_path(
                &root,
                CleanupPhase::Pending,
                operation_id,
            )),
            Some((CleanupPhase::Ready, _)) => {
                bail!("cannot cancel an activated cleanup operation")
            }
        }
    }

    fn retry_cleanup(
        &self,
        cleanup: &PreparedCleanup,
        profiles: &Profiles,
    ) -> anyhow::Result<CleanupOutcome> {
        let root = self.ensure_materialization_layout()?;
        let operation_id = cleanup.operation_id();
        let Some((phase, journal)) = Self::locate_cleanup(&root, operation_id)? else {
            return Ok(CleanupOutcome::AlreadyAbsent);
        };
        if phase == CleanupPhase::Pending {
            self.activate_cleanup(cleanup)?;
        }

View on GitHub (pinned to f7dbce2997)

Solutions

  1. Do not cancel a Ready cleanup: instead let it execute, or use the compensation/retry path to run and clear it
  2. Poll the journal state before cancelling: only issue cancel when the recorded phase is Pending
  3. If the cleanup must not run, examine whether a rollback of the promotion (compensate) is the correct operation instead of cancel
  4. Check for code paths that promote the phase too early (activation race) and serialize cancel against activation under the same lock/actor

Example fix

// before: unconditional cancel
manager.cancel_cleanup(operation_id)?;
// after: only cancel while still pending
let phase = manager.cleanup_phase(operation_id)?;
if phase == CleanupPhase::Pending {
    manager.cancel_cleanup(operation_id)?;
} else {
    manager.compensate(operation_id)?;
}
Defensive patterns

Strategy: validation

Validate before calling

let phase = manager.cleanup_phase(operation_id)?;
if phase != CleanupPhase::Pending {
    // cannot cancel; route to compensation instead
    return Err(anyhow::anyhow!("cleanup already activated"));
}
manager.cancel_cleanup(operation_id)?;

Type guard

fn is_cancellable(phase: CleanupPhase) -> bool {
    matches!(phase, CleanupPhase::Pending)
}

Try / catch

match manager.cancel_cleanup(op_id) {
    Err(e) if e.to_string().contains("cannot cancel an activated cleanup") => {
        manager.compensate(op_id)?; // activated: compensate instead
    }
    other => other?,
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

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 libnyanpasu/clash-nyanpasu@f7dbce2997 (2026-09-08). Data as JSON: /api/errors/18d623afb5ba3a4f. Report an issue: GitHub.