libnyanpasu/clash-nyanpasu · error

cannot promote a compensating materialization

Error message

cannot promote a compensating materialization

What it means

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.

Source

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

    }

    fn promote(&self, prepared: &PreparedMaterialization) -> anyhow::Result<()> {
        let root = self.ensure_materialization_layout()?;
        let operation_id = prepared.operation_id();
        let Some((mut location, journal)) = self.locate_materialization(&root, operation_id)?
        else {
            bail!("materialization journal not found for operation {operation_id}");
        };

        if let Some(promoting) = location.promoting() {
            Self::transition_journal(&root, operation_id, location, promoting)?;
            location = promoting;
        }
        if matches!(
            location,
            JournalLocation::StateCompensating | JournalLocation::FileCompensating
        ) {
            bail!("cannot promote a compensating materialization");
        }

        let target = self.resolve(&journal.managed_path)?;
        if Self::path_hash(&target)? != journal.hash {
            self.promote_resource(&root, operation_id, &target, &journal.hash)?;
        }
        if location == JournalLocation::FilePromoting {
            Self::transition_journal(
                &root,
                operation_id,
                JournalLocation::FilePromoting,
                JournalLocation::FilePromoted,
            )?;
        }
        Ok(())
    }

    fn complete(&self, prepared: &PreparedMaterialization) -> anyhow::Result<()> {

View on GitHub (pinned to f7dbce2997)

Solutions

  1. 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.
  2. Fix control flow so a failed operation is compensated once and then re-prepared, never re-promoted.
  3. Serialize operations per profile path so concurrent promote/compensate on the same operation cannot interleave.
  4. Check the journal phase (locate_materialization) before deciding to promote, and route compensating phases to the compensate path.

Example fix

// before
match op.run() {
    Err(_) => service.compensate(&prepared)?,
}
service.promote(&prepared)?; // wrong: promoting a compensating op

// after
match op.run() {
    Err(_) => {
        service.compensate(&prepared)?;
        let prepared = service.prepare_state_first(&path, resource, revision)?;
    }
    _ => {}
}
service.promote(&prepared)?;
Defensive patterns

Strategy: validation

Validate before calling

if let Ok(Some((location, _journal))) = service.locate_materialization(&root, prepared.operation_id()) {
    anyhow::ensure!(
        !matches!(location, JournalLocation::StateCompensating | JournalLocation::FileCompensating),
        "operation {} is compensating; must re-prepare before promoting",
        prepared.operation_id()
    );
}

Type guard

fn is_compensating(loc: &JournalLocation) -> bool {
    matches!(loc, JournalLocation::StateCompensating | JournalLocation::FileCompensating)
}

Try / catch

if let Err(e) = service.promote(&prepared) {
    if e.to_string().contains("compensating") {
        service.compensate(&prepared)?;
        let prepared = service.prepare_state_first(&path, resource, revision)?;
        service.promote(&prepared)?;
    } else { return Err(e); }
}

Prevention

When it happens

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

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

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/5361d38382fac3df. Report an issue: GitHub.