fix(download): never leave partial files behind - #165
Conversation
Interrupted downloads and failed atomic writes left large partial files on disk that nothing ever removed. Because each temp name embeds a timestamp, every retry leaked a distinct orphan — so on a full disk each attempt consumed more of the little space that was left. Stream downloads to a sibling .part file and rename into place only once the body is complete, removing the partial on every failure path. This also stops a truncated transfer from landing at the destination, where the lemonade engine would accept it as a valid cache entry forever. Clean up the temp file on the failure paths of both write_file_atomically copies, and preserve full file names so sdk.tar.gz no longer becomes sdk.tar.tmp-<id>. The SDK tarball is now streamed rather than read fully into memory, which removes a multi-gigabyte allocation on the install path. Refs #158 Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
The two dashboard scenarios that open Observe send the `4` tab key straight after launching the TUI, with no assertion in between. A key written into the pseudo-terminal before the dashboard is reading input can be consumed by whatever holds the terminal at that moment, and nothing ever retries it — so the dashboard stays on Home and the scenario fails 30s later in an assertion about a view it never left. Send the key until the Observe chip is actually marked active, so the step depends on the dashboard having acted on the key rather than on it having been ready when the key was written. Reproduced by pointing ROCM_CLI_BINARY at a wrapper that drains the terminal before exec'ing the real binary: the scenario failed with exactly the CI symptom before this change and passes after it. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
8c1ec0c to
7204154
Compare
Merge-queue ejection: root cause and fixThis PR was ejected from the merge queue when the blocking Root causeThe captured screen in that log shows the dashboard still on Home — the The two scenarios that open Observe launch the TUI and send the tab key with no assertion in between, unlike the demo-data journeys which assert the home view first. A key written into the pseudo-terminal before the dashboard is reading input can be consumed by whatever holds the terminal at that moment, and nothing retries it. The step then fails much later, in an assertion about a view the dashboard never left — which is exactly the failure signature above. This is a latent synchronisation defect in the E2E step, not in this PR's production code: nothing in the download/atomic-write change is reachable from FixSend the tab key repeatedly until the Observe chip is actually marked active, so the step depends on the dashboard having acted on the key rather than on it having been ready when the key was written. The helper is restricted to idempotent keys (a tab jump, not a toggle). ReproductionDeterministic, by pointing the harness at a wrapper that drains the terminal before exec'ing the real binary — i.e. a dashboard that is not yet reading when the key arrives: cat > /tmp/wrapper/rocm <<'SH'
#!/bin/sh
stty raw -echo min 0 time 20; dd bs=1 count=16 of=/dev/null 2>/dev/null; stty sane
exec /path/to/target/release/rocm "$@"
SH
ROCM_CLI_BINARY=/tmp/wrapper/rocm cargo xtask e2e -- -n "Observe displays metrics"Before: Verified in addition: full Note: the guard is exercised by these two scenarios on every run, but the key-loss condition itself is only reproducible with the wrapper above, so there is no automated test that would fail without it. |
Summary
Downloads and atomic writes no longer leave partial files behind.
.partfile and rename into place only once the body is complete, removing the partial on every failure path.write_file_atomicallycopies.sdk.tar.gzno longer becomessdk.tar.tmp-<id>.Root cause
Two separate defects, both reproduced against this tree.
write_file_atomicallynamed its temppath.tmp-<unix_ms>and returned early on a write error without removing it. Because the name embeds a timestamp, each retry left a distinct orphan rather than reusing one. Filling a small filesystem and calling it twice produced two orphans, the first holding all the space that remained — so on a full disk, retrying made things worse:download_file_to_pathwrote straight to the final path with no temp and no cleanup, so an interrupted transfer left a truncated file exactly where callers look for a complete one. Serving aContent-Lengthof 1 MiB and then sending 16 bytes left those 16 bytes at the destination.Tests
download_leaves_no_truncated_file_at_destination— a body that ends early leaves neither a destination file nor a partial.stream_to_path_atomically_writes_complete_body— the happy path renames into place and keeps every extension.temp_sibling_path_preserves_multi_dot_file_names.write_file_atomically_cleans_up_temp_on_write_failurein both crates, marked#[ignore]because they fill/dev/shmto provoke ENOSPC and so are not safe to run concurrently. Run withcargo test -- --ignored.The first two run by default and are portable. I ran the original failing reproduction against unpatched code first, so these are fails-before/passes-after rather than assertions written after the fact.
Notes
This is the generic fix in the shared download helper, so it covers every caller. #143 separately adds SHA-256 validation for the Lemonade archive specifically; the two are complementary and touch different files — this one stops a truncated file from ever landing at the destination, that one catches a bad file that got there another way.
Fixes #158