Skip to content

Directory downloads should retry into a fresh staging directory #4805

Open

Description

@Aliciapet11

Summary

sandbox_sync_down_file extracts each attempt into a TempDir in the destination parent and then moves the completed file into place. sandbox_sync_down_directory extracts directly into the final destination.

Because retry_sandbox_sync treats failed to extract tar archive from sandbox as retryable, a directory-download failure can retry into a destination that already contains a partially extracted tree. This can make otherwise recoverable retries fail on pre-existing entries and leaves the caller with a dirty destination.

Observed context

In the linked incident, the destination was cleared before the first download, but the first internal extraction attempt failed. The Display-form error did not expose the underlying cause, so it was not possible to determine whether the failure was transient.

Request

For directory downloads, please stage each retry in a fresh TempDir in the destination parent, similar to sandbox_sync_down_file, and publish the completed tree only after extraction succeeds. This would make internal retries idempotent and allow downstream callers to avoid an extra outer retry.

Alternatively, please stop treating deterministic extraction failures such as pre-existing entries as retryable, or expose enough structured error detail for callers to distinguish transient extraction failures from deterministic ones.

Activity

  1. O96a commented on Oct 21, 2026

    @O96a

    The directory path needs the same staging treatment as the file path, but the staging dir has to be created in the destination's parent, not in TempDir — rename is only atomic within a filesystem, and /tmp will hand you EXDEV the first time the destination is on a mounted volume.

    The swap is where it actually gets fiddly, since rename(staging, dest) fails with ENOTEMPTY when dest already exists:

    // staging = dest.parent().join(format!(".{}.staging.{}", name, attempt))
    extract_tar(archive, &staging)?;          // full extract, no partial dest
    if dest.exists() {
        fs::rename(&dest, &dest_old)?;        // dest -> dest.old
    }
    fs::rename(&staging, &dest)?;             // staging -> dest
    let _ = fs::remove_dir_all(&dest_old);    // best effort

    Two things that bite if you skip them: the staging dir has to be per-attempt and removed on the failure path, otherwise the retry inherits the previous attempt's partial tree and you've just relocated the bug rather than fixed it. And on startup, sweep any leftover dest.old — if the process died between the two renames you'll otherwise leave a stale tree sitting next to the destination forever.

    Separately, I'd narrow what retry_sandbox_sync considers retryable. A tar extraction failure is usually a truncated stream and worth another attempt; a checksum mismatch or a path-traversal rejection is deterministic and retrying it just burns the deadline.

  2. Aliciapet11 commented on Oct 22, 2026

    @Aliciapet11
    Author

    Thanks - agreed. To clarify, the intent is tempfile::TempDir::new_in(destination_parent), matching the file path, so staging and final destination remain on the same filesystem and avoid EXDEV.
    Per-attempt staging plus failure cleanup is also required, and narrowing retryable errors makes sense so deterministic checksum/path failures don’t consume the retry budget.
    Fullsend currently works around this by staging each outer attempt in the destination parent; once OpenShell handles directory retries internally, we can remove that outer retry.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    state:triage-neededOpened without agent diagnostics and needs triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions