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
- 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.
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
- 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.
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
- materialization is not in a completable phase
- materialization journal not found for operation {operation_i
- cannot complete materialization with a target hash mismatch
AI-assisted analysis of libnyanpasu/clash-nyanpasu@f7dbce2997 (2026-09-08).
Data as JSON: /api/errors/5361d38382fac3df.
Report an issue: GitHub.