Skip to content

Commit e9cb95a

Browse files
committed
fix(simulator): AOT cache publish survives process exit
1 parent 9fa0c66 commit e9cb95a

1 file changed

Lines changed: 108 additions & 13 deletions

File tree

  • crates/simulator/src/backend/aot_c

crates/simulator/src/backend/aot_c/emit.rs

Lines changed: 108 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2865,7 +2865,7 @@ fn compile_source_in(cache_dir: &Path, src: &str) -> Result<EmittedModule, Strin
28652865
if !so_path.exists() {
28662866
// Identical sources hash to the same `so_path`, so a `cc -o so_path`
28672867
// from one thread can be dlopened half-written by another. Compile to a
2868-
// unique temp, then `rename` (atomic within the dir) to publish.
2868+
// unique temp, then `rename`/`mv` (atomic within the dir) to publish.
28692869
use std::sync::atomic::{AtomicU64, Ordering};
28702870
static TMP_CTR: AtomicU64 = AtomicU64::new(0);
28712871
let uniq = format!(
@@ -2878,14 +2878,54 @@ fn compile_source_in(cache_dir: &Path, src: &str) -> Result<EmittedModule, Strin
28782878
let tmp_so = cache_dir.join(format!("veryl_aot_{hash}.{uniq}.so"));
28792879
std::fs::write(&tmp_c, src).map_err(|e| format!("write {}: {}", tmp_c.display(), e))?;
28802880

2881-
let mut cmd = Command::new(&cc_name);
2882-
cmd.args(&flags).arg("-o").arg(&tmp_so).arg(&tmp_c);
2883-
2884-
let out = cmd
2885-
.output()
2886-
.map_err(|e| format!("spawn cc: {e} (set VERYL_AOT_CC to override)"))?;
2881+
// The compile AND the publish run through one shell so the cache
2882+
// entry lands even when this process exits first: a short run
2883+
// finishes before cc does and the pool worker dies with it, so a
2884+
// rename on the Rust side would discard the orphaned cc's output
2885+
// and leave every rerun on the JIT path. Orphan reparenting keeps
2886+
// the shell running. Positional parameters keep the paths out of
2887+
// shell-quoting territory.
2888+
#[cfg(unix)]
2889+
let out = {
2890+
let mut cmd = Command::new("/bin/sh");
2891+
cmd.arg("-c")
2892+
.arg(
2893+
r#"cc="$1"; tso="$2"; tc="$3"; pc="$4"; pso="$5"; shift 5; "$cc" "$@" -o "$tso" "$tc" || { rm -f "$tso"; exit 1; }; mv -f "$tc" "$pc"; mv -f "$tso" "$pso""#,
2894+
)
2895+
.arg("sh")
2896+
.arg(&cc_name)
2897+
.arg(&tmp_so)
2898+
.arg(&tmp_c)
2899+
.arg(&c_path)
2900+
.arg(&so_path)
2901+
.args(&flags);
2902+
// Own process group: a group-delivered signal (Ctrl-C on the
2903+
// run, a harness killing its group) must not take the publish
2904+
// down with it.
2905+
{
2906+
use std::os::unix::process::CommandExt;
2907+
cmd.process_group(0);
2908+
}
2909+
cmd.output()
2910+
.map_err(|e| format!("spawn sh/cc: {e} (set VERYL_AOT_CC to override)"))?
2911+
};
2912+
#[cfg(not(unix))]
2913+
let out = {
2914+
let mut cmd = Command::new(&cc_name);
2915+
cmd.args(&flags).arg("-o").arg(&tmp_so).arg(&tmp_c);
2916+
let out = cmd
2917+
.output()
2918+
.map_err(|e| format!("spawn cc: {e} (set VERYL_AOT_CC to override)"))?;
2919+
if out.status.success() {
2920+
// A racing peer publishes an equally valid file (same
2921+
// source), so an overwrite either way is fine.
2922+
let _ = std::fs::rename(&tmp_c, &c_path);
2923+
std::fs::rename(&tmp_so, &so_path)
2924+
.map_err(|e| format!("rename {}: {}", tmp_so.display(), e))?;
2925+
}
2926+
out
2927+
};
28872928
if !out.status.success() {
2888-
let _ = std::fs::remove_file(&tmp_so);
28892929
// Leave the temp .c for inspection.
28902930
return Err(format!(
28912931
"cc {} failed: {}\n{}",
@@ -2894,11 +2934,6 @@ fn compile_source_in(cache_dir: &Path, src: &str) -> Result<EmittedModule, Strin
28942934
String::from_utf8_lossy(&out.stderr),
28952935
));
28962936
}
2897-
// A racing peer publishes an equally valid file (same source), so an
2898-
// overwrite either way is fine.
2899-
let _ = std::fs::rename(&tmp_c, &c_path);
2900-
std::fs::rename(&tmp_so, &so_path)
2901-
.map_err(|e| format!("rename {}: {}", tmp_so.display(), e))?;
29022937
}
29032938

29042939
// SAFETY: the .so was just compiled by us (or previously cached) and
@@ -7637,6 +7672,66 @@ mod tests {
76377672
/// Compile `src` end-to-end; return `None` when the built `.so`
76387673
/// can't load on this host (e.g. cross-arch `cc` on Windows-on-ARM).
76397674
/// Genuine compile failures still panic.
7675+
#[test]
7676+
#[cfg(unix)]
7677+
fn aot_cache_publish_survives_process_exit() {
7678+
// A short run exits while cc is still going, so the publish must not
7679+
// depend on this process surviving to rename the temp files. The
7680+
// test re-executes itself as a child that starts one compile and
7681+
// exits; the parent then waits for the artifact to appear.
7682+
const CHILD_DIR: &str = "VERYL_TEST_AOT_PUBLISH_DIR";
7683+
if let Ok(dir) = std::env::var(CHILD_DIR) {
7684+
let dir = PathBuf::from(dir);
7685+
std::thread::spawn(move || {
7686+
let _ = compile_source_in(&dir, "// AOT-C publish probe\n");
7687+
});
7688+
std::thread::sleep(std::time::Duration::from_millis(200));
7689+
std::process::exit(0);
7690+
}
7691+
7692+
let tmp = std::env::temp_dir().join(format!("veryl_aot_pub_{}", std::process::id()));
7693+
let _ = std::fs::remove_dir_all(&tmp);
7694+
std::fs::create_dir_all(&tmp).unwrap();
7695+
// A cc that outlives the child before writing its output.
7696+
let slow_cc = tmp.join("slow_cc.sh");
7697+
std::fs::write(
7698+
&slow_cc,
7699+
"#!/bin/sh\nsleep 2\nwhile [ $# -gt 0 ]; do [ \"$1\" = -o ] && out=$2; shift; done\n: > \"$out\"\n",
7700+
)
7701+
.unwrap();
7702+
use std::os::unix::fs::PermissionsExt;
7703+
std::fs::set_permissions(&slow_cc, std::fs::Permissions::from_mode(0o755)).unwrap();
7704+
7705+
let status = Command::new(std::env::current_exe().unwrap())
7706+
.arg("aot_cache_publish_survives_process_exit")
7707+
.env(CHILD_DIR, &tmp)
7708+
.env("VERYL_AOT_CC", &slow_cc)
7709+
.stdout(std::process::Stdio::null())
7710+
.stderr(std::process::Stdio::null())
7711+
.status()
7712+
.unwrap();
7713+
assert!(status.success(), "child run failed");
7714+
7715+
// Published artifacts are `veryl_aot_<hash>.so`; the temps carry an
7716+
// extra `.<pid>.<n>` and must not count.
7717+
let published = || {
7718+
std::fs::read_dir(&tmp)
7719+
.into_iter()
7720+
.flatten()
7721+
.flatten()
7722+
.any(|e| {
7723+
let n = e.file_name().to_string_lossy().into_owned();
7724+
n.starts_with("veryl_aot_") && n.ends_with(".so") && n.matches('.').count() == 1
7725+
})
7726+
};
7727+
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
7728+
while !published() && std::time::Instant::now() < deadline {
7729+
std::thread::sleep(std::time::Duration::from_millis(100));
7730+
}
7731+
assert!(published(), "the .so must publish after the run exits");
7732+
let _ = std::fs::remove_dir_all(&tmp);
7733+
}
7734+
76407735
fn compile_for_test(cache_dir: &Path, src: &str, what: &str) -> Option<EmittedModule> {
76417736
match compile_source_in(cache_dir, src) {
76427737
Ok(m) => Some(m),

0 commit comments

Comments
 (0)