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
- Check for another running updater instance and let it finish before retrying
- Run the recovery path (recover_executable_transaction_locked) to commit or roll back the pending journal, then retry
- Inspect the journal JSON in install_dir to decide whether to complete or roll back manually
- 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
- Serialize updates with a process/machine-wide single-instance lock
- Always run recovery after a crashed or interrupted update
- Alert on leftover journal files in the install directory
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.