astrid-runtime/astrid · warning

AlreadyExists

AlreadyExists

Error message

an executable replacement transaction is already pending

What it means

Executable replacement is a journal-based transaction: before writing a new journal, write_transaction_journal checks whether TRANSACTION_JOURNAL already exists in the install directory. If it does, a previous replacement transaction is still pending (in progress, interrupted, or not recovered), and starting another would corrupt the transaction protocol, so it fails with AlreadyExists.

Source

Thrown at crates/astrid-core/src/platform_fs/windows/executable.rs:267

            rollback,
            legacy_displaced: None,
            had_live,
            old_hash,
            new_hash,
        });
    }
    cleanup.disarm();
    Ok(journal)
}

pub(super) fn write_transaction_journal(
    install_dir: &Path,
    journal: &ExecutableTransaction,
    guard: &TrustedPathGuard,
) -> io::Result<()> {
    let journal_path = install_dir.join(TRANSACTION_JOURNAL);
    if guarded_file_exists(guard, &journal_path)? {
        return Err(io::Error::new(
            io::ErrorKind::AlreadyExists,
            "an executable replacement transaction is already pending",
        ));
    }
    let bytes = serde_json::to_vec(journal)
        .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
    let (staged, journal_file) =
        stage_unique_bytes_retained(guard, install_dir, &bytes, "astrid-update-journal")?;
    #[cfg(test)]
    if let Err(error) = super::io::test_maybe_fail_journal_rename() {
        drop(journal_file);
        let _ = remove_guarded_file(guard, &staged);
        return Err(error);
    }
    if let Err(error) = move_guarded_file(guard, &staged, &journal_path) {
        drop(journal_file);
        let _ = remove_guarded_file(guard, &staged);
        return Err(error);

View on GitHub (pinned to affd8760f4)

Solutions

  1. Check for another running updater instance and let it finish before retrying
  2. Run the recovery path (recover_executable_transaction_locked) to commit or roll back the pending journal, then retry
  3. Inspect the journal JSON in install_dir to decide whether to complete or roll back manually
  4. Only delete the journal file if you have confirmed the transaction is fully applied or safely discardable

Example fix

// before: blind retry
replace_executable_set(...)?;
// after: recover a pending journal before starting a new transaction
if install_dir.join(TRANSACTION_JOURNAL).exists() {
    recover_executable_transaction_locked(&guard, &install_dir)?; // commit or roll back
}
replace_executable_set(...)?;
Defensive patterns

Strategy: try-catch

Validate before calling

// Detect a pending journal before starting a replacement
fn transaction_pending(install_dir: &std::path::Path) -> bool {
    install_dir.join("astrid-update-journal").exists()
        || install_dir.join(TRANSACTION_JOURNAL).exists()
}

Try / catch

match replace_executable_set(...) {
    Err(e) if e.kind() == io::ErrorKind::AlreadyExists
        && e.to_string().contains("already pending") => {
        recover_executable_transaction_locked(&guard, &install_dir)?; // commit/roll back, then retry
    }
    other => other?,
}

Prevention

When it happens

Trigger: Calling replace_executable_set when install_dir already contains a transaction journal file — from a concurrently running update or from a previous crashed/killed update that was never recovered via recover_executable_transaction_locked.

Common situations: Two updater instances racing on the same install; a prior update crashed before commit/rollback and left the journal on disk; recovery logic never ran after a power loss mid-update.

Understand the failure class

Background: "already exists" / EEXIST / FileAlreadyExistsException: what the 'file already exists' error means and how to fix it — this error's family across 37 libraries.

Related errors


AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09). Data as JSON: /api/errors/1e1612e54243588d. Report an issue: GitHub.