From 263236fd8219ffe4e98bfca690dfbd6eadff78c4 Mon Sep 17 00:00:00 2001 From: Lucas Vieira Date: Mon, 14 Sep 2026 02:02:13 -0300 Subject: [PATCH 1/2] fix(codebuild): sweep only build containers whose owning process is gone At startup CodeBuild removed every `fakecloud-codebuild` container whose `fakecloud-instance` label was not the current process -- including those of other fakecloud processes that are still running and sharing the daemon. Any second server starting (parallel e2e test servers, side-by-side installs) killed the first server's in-flight build container, failing the build in PROVISIONING with "failed to create /codebuild/build: " (the `docker exec` into a container that had just been removed). The codebuild_real_execution e2e tests hit exactly that when run in parallel. The shared startup reaper already got this right by checking whether the owning PID is alive. Move that check to fakecloud_core::container_net (`pid_alive` plus `owned_by_dead_process`) and use it from both, so a container is swept only when its owner is neither this process nor alive. Also: ec2_instance_runtime's "container should be running" assertion now reports the container's status, exit code, OOM flag, error, start/finish time and logs, so its remaining intermittent failure names its cause. Tests: owned_by_dead_process unit test; a new e2e starts a second server while a build runs and asserts the build still succeeds (fails with the build FAILED under the old sweep). --- crates/fakecloud-codebuild/src/runtime.rs | 14 ++-- crates/fakecloud-core/Cargo.toml | 3 + crates/fakecloud-core/src/container_net.rs | 66 +++++++++++++++++++ .../tests/codebuild_real_execution.rs | 57 ++++++++++++++++ .../tests/ec2_instance_runtime.rs | 14 +++- crates/fakecloud-server/src/reaper.rs | 38 ++--------- 6 files changed, 153 insertions(+), 39 deletions(-) diff --git a/crates/fakecloud-codebuild/src/runtime.rs b/crates/fakecloud-codebuild/src/runtime.rs index 7dfd4a3d9..8a5e88f6a 100644 --- a/crates/fakecloud-codebuild/src/runtime.rs +++ b/crates/fakecloud-codebuild/src/runtime.rs @@ -226,11 +226,12 @@ fn current_instance_label() -> String { /// build containers survive: `reconcile_builds` flips the persisted /// `IN_PROGRESS` builds to `FAILED`, but nothing reaps the containers, so the /// daemon slowly leaks them. Sweep every container tagged `fakecloud-codebuild` -/// whose `fakecloud-instance` label is not the current process. Best-effort: -/// any daemon error is ignored (the backend stays usable). Containers owned by -/// the live process are left untouched so a concurrent build is never killed. +/// whose owning process (its `fakecloud-instance` label) is gone. Containers +/// owned by any *live* fakecloud process are left untouched: several can share +/// one daemon, and removing another running server's build container fails +/// that build mid-provisioning. Best-effort: any daemon error is ignored (the +/// backend stays usable). pub async fn sweep_orphan_containers(cli: &str) { - let current = current_instance_label(); let out = Command::new(cli) .args([ "ps", @@ -254,7 +255,10 @@ pub async fn sweep_orphan_containers(cli: &str) { continue; }; let instance = parts.next().unwrap_or(""); - if instance != current { + if fakecloud_core::container_net::owned_by_dead_process( + instance, + fakecloud_core::container_net::pid_alive, + ) { orphans.push(id.to_string()); } } diff --git a/crates/fakecloud-core/Cargo.toml b/crates/fakecloud-core/Cargo.toml index d52efa7ab..0b27c2438 100644 --- a/crates/fakecloud-core/Cargo.toml +++ b/crates/fakecloud-core/Cargo.toml @@ -29,3 +29,6 @@ tokio = { workspace = true } tokio-util = { workspace = true } tracing = { workspace = true } uuid = { workspace = true } + +[target.'cfg(unix)'.dependencies] +libc = "0.2" diff --git a/crates/fakecloud-core/src/container_net.rs b/crates/fakecloud-core/src/container_net.rs index f4bf39be7..6d099f498 100644 --- a/crates/fakecloud-core/src/container_net.rs +++ b/crates/fakecloud-core/src/container_net.rs @@ -158,6 +158,47 @@ pub fn bounded_status(cli: &str, args: &[String]) -> bool { wait_bounded(&mut child) && child.wait().map(|s| s.success()).unwrap_or(false) } +/// True if the given PID is a live process on this host. +/// +/// On Unix this is `kill(pid, 0)`: it returns 0 if the process exists +/// (including zombies), or sets `errno` to `ESRCH` if not. On non-Unix +/// platforms it conservatively returns `true`, so a caller never removes a +/// resource it can't prove is orphaned. +#[cfg(unix)] +pub fn pid_alive(pid: u32) -> bool { + // SAFETY: `kill` with signal 0 is a liveness probe; it does not + // actually deliver a signal. Any PID value is safe to pass. + let rc = unsafe { libc::kill(pid as libc::pid_t, 0) }; + if rc == 0 { + return true; + } + // errno == EPERM means the process exists but we can't signal it — + // still alive from our perspective. + std::io::Error::last_os_error().raw_os_error() == Some(libc::EPERM) +} + +#[cfg(not(unix))] +pub fn pid_alive(_pid: u32) -> bool { + true +} + +/// Whether a container or network labelled `fakecloud-instance=