Skip to content

Commit c31ef62

Browse files
committed
fixup! cache issues when dl files
Signed-off-by: Max Olender <molender@nvidia.com>
1 parent 1fd9af3 commit c31ef62

3 files changed

Lines changed: 101 additions & 12 deletions

File tree

crates/rvs/src/artifact/io.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,10 @@ async fn download_one(
128128
) -> Result<(), RvsError> {
129129
let path = std::path::Path::new(&artifact.output_path);
130130

131+
// Cache hit: trust that a non-tmp file at `path` is complete, because we
132+
// only rename into place after a fully streamed body (and checksum, when
133+
// advertised) succeeds. We do NOT re-verify on hit -- a stable URL is
134+
// assumed to map to stable bytes for the lifetime of the cache.
131135
if path.exists() {
132136
tracing::debug!(path = artifact.output_path, "artifact: cache hit, skipping");
133137
return Ok(());
@@ -159,7 +163,12 @@ async fn download_one(
159163
.and_then(|v| v.to_str().ok())
160164
.map(str::to_lowercase);
161165

162-
let mut file = tokio::fs::File::create(path).await?;
166+
// Stream to a sibling `.partial` file and rename on success, so an
167+
// interrupted download never poisons the cache with a truncated file.
168+
// Append (not `with_extension`) so `foo.bin` and `foo.json` get distinct
169+
// tmp paths instead of colliding on `foo.partial`.
170+
let tmp_path = std::path::PathBuf::from(format!("{}.partial", artifact.output_path));
171+
let mut file = tokio::fs::File::create(&tmp_path).await?;
163172
let mut hasher = Sha256::new();
164173
let mut stream = response.bytes_stream();
165174
while let Some(chunk) = stream.next().await {
@@ -168,19 +177,22 @@ async fn download_one(
168177
hasher.update(&chunk);
169178
file.write_all(&chunk).await?;
170179
}
180+
file.flush().await?;
171181

172182
if let Some(expected) = expected_sha256 {
173183
let actual = hex::encode(hasher.finalize());
174184
if actual != expected {
185+
let _ = tokio::fs::remove_file(&tmp_path).await;
175186
return Err(RvsError::ChecksumMismatch {
176187
path: artifact.output_path.clone(),
177188
expected,
178189
actual,
179190
});
180191
}
181-
tracing::debug!(path = artifact.output_path, "artifact: checksum OK");
192+
tracing::info!(path = artifact.output_path, "artifact: checksum OK");
182193
}
183194

195+
tokio::fs::rename(&tmp_path, path).await?;
184196
Ok(())
185197
}
186198

crates/rvs/src/bin/carbide-rvs.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -109,13 +109,13 @@ async fn main() -> Result<(), RvsError> {
109109
let validation_cancel_token = cancel_token.clone();
110110

111111
tokio::spawn(async move {
112+
let Ok(mut sigint) = signal(SignalKind::interrupt()) else {
113+
return;
114+
};
115+
let Ok(mut sigterm) = signal(SignalKind::terminate()) else {
116+
return;
117+
};
112118
loop {
113-
let Ok(mut sigint) = signal(SignalKind::interrupt()) else {
114-
break;
115-
};
116-
let Ok(mut sigterm) = signal(SignalKind::terminate()) else {
117-
break;
118-
};
119119
// Wait for SIGINT or SIGTERM
120120
let received_signal = tokio::select! {
121121
_ = sigint.recv() => "SIGINT",

crates/rvs/src/scenario/mod.rs

Lines changed: 81 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ pub struct Artifact {
2727
pub name: String,
2828
pub output: String,
2929
/// Direct download URL (mutually exclusive with `sotpath`).
30-
// TODO[#416]: enforce exactly one of `uri`/`sotpath` is set - add a
31-
// post-deserialization validation step in Scenario::load or a custom
32-
// Deserialize impl. Currently both can be set (or neither) without error.
30+
///
31+
/// Exactly one of `uri`/`sotpath` must be set; enforced in
32+
/// `Scenario::load` after deserialization.
3333
pub uri: Option<String>,
3434
/// JSONPath into SOT JSON to resolve download URL.
3535
pub sotpath: Option<String>,
@@ -74,7 +74,33 @@ impl Scenario {
7474
pub fn load(path: &Path) -> Result<Self, String> {
7575
let content =
7676
std::fs::read_to_string(path).map_err(|e| format!("read {}: {e}", path.display()))?;
77-
toml::from_str(&content).map_err(|e| format!("parse {}: {e}", path.display()))
77+
let scenario: Scenario =
78+
toml::from_str(&content).map_err(|e| format!("parse {}: {e}", path.display()))?;
79+
scenario
80+
.validate()
81+
.map_err(|e| format!("validate {}: {e}", path.display()))?;
82+
Ok(scenario)
83+
}
84+
85+
fn validate(&self) -> Result<(), String> {
86+
for artifact in &self.artifacts {
87+
match (&artifact.uri, &artifact.sotpath) {
88+
(Some(_), Some(_)) => {
89+
return Err(format!(
90+
"artifact '{}': both 'uri' and 'sotpath' set; exactly one required",
91+
artifact.name
92+
));
93+
}
94+
(None, None) => {
95+
return Err(format!(
96+
"artifact '{}': neither 'uri' nor 'sotpath' set; exactly one required",
97+
artifact.name
98+
));
99+
}
100+
_ => {}
101+
}
102+
}
103+
Ok(())
78104
}
79105
}
80106

@@ -96,4 +122,55 @@ mod tests {
96122
assert_eq!(scenario.teardown.len(), 1);
97123
assert_eq!(scenario.test[0].name, "nv_basic");
98124
}
125+
126+
fn scenario_with_artifacts(artifacts: Vec<Artifact>) -> Scenario {
127+
Scenario {
128+
rack: RackTarget {
129+
model: "gb200nvl".to_string(),
130+
sot_release: "1.2.5".to_string(),
131+
},
132+
os: OsImage { uri: "https://example.com/os.img".to_string() },
133+
artifacts,
134+
setup: vec![],
135+
test: vec![],
136+
teardown: vec![],
137+
}
138+
}
139+
140+
fn artifact(name: &str, uri: Option<&str>, sotpath: Option<&str>) -> Artifact {
141+
Artifact {
142+
name: name.to_string(),
143+
output: format!("{name}.bin"),
144+
uri: uri.map(str::to_string),
145+
sotpath: sotpath.map(str::to_string),
146+
}
147+
}
148+
149+
#[test]
150+
fn validate_accepts_uri_only() {
151+
let s = scenario_with_artifacts(vec![artifact("a", Some("https://x/y"), None)]);
152+
assert!(s.validate().is_ok());
153+
}
154+
155+
#[test]
156+
fn validate_accepts_sotpath_only() {
157+
let s = scenario_with_artifacts(vec![artifact("a", None, Some("$.foo"))]);
158+
assert!(s.validate().is_ok());
159+
}
160+
161+
#[test]
162+
fn validate_rejects_both_uri_and_sotpath() {
163+
let s = scenario_with_artifacts(vec![artifact("a", Some("https://x/y"), Some("$.foo"))]);
164+
let err = s.validate().unwrap_err();
165+
assert!(err.contains("both"), "got: {err}");
166+
assert!(err.contains("'a'"), "got: {err}");
167+
}
168+
169+
#[test]
170+
fn validate_rejects_neither_uri_nor_sotpath() {
171+
let s = scenario_with_artifacts(vec![artifact("a", None, None)]);
172+
let err = s.validate().unwrap_err();
173+
assert!(err.contains("neither"), "got: {err}");
174+
assert!(err.contains("'a'"), "got: {err}");
175+
}
99176
}

0 commit comments

Comments
 (0)