From fec605d7f87362d3dca1377e73a8024c8b49d549 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 7 Sep 2026 01:21:02 +0200 Subject: [PATCH] An empty route fragment is absence, not a membership The first deployment of any routed target could never succeed, and wedged itself permanently on the way down. The route preflight bind-mounts its candidate over config_path to run `nginx -t` in a private mount namespace. When that path does not exist yet, systemd materializes the mount point and leaves an empty file on the host. Read as content, that made the baseline "change" during validation, failing the transaction -- and every retry then saw a zero-length fragment where the transaction expected none and reported an unadmitted incumbent, which no amount of retrying could clear. A fragment with no bytes grants no route, so it is now read as absence. That makes the preflight idempotent whether or not systemd materialized the mount point, and lets a first deployment retry cleanly. Co-Authored-By: Claude Opus 5 --- src/drivers.rs | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/drivers.rs b/src/drivers.rs index d708535..13c11cb 100644 --- a/src/drivers.rs +++ b/src/drivers.rs @@ -4044,6 +4044,15 @@ impl NginxRouteDriver { fn current_configuration(&self) -> Result>> { Ok(match fs::read(&self.binding.config_path) { + // A fragment with no bytes grants no route, so it is absence, not a + // membership of zero servers. The distinction matters on the first + // deployment of a routed target: the preflight bind-mounts its + // candidate over config_path, and systemd materializes that mount + // point, leaving an empty file behind on the host. Read as content, + // it made the baseline "change" during validation, and then made + // every retry report an unadmitted incumbent -- the target could + // never be deployed a first time, and the wedge was permanent. + Ok(bytes) if bytes.is_empty() => None, Ok(bytes) => Some(bytes), Err(error) if error.kind() == ErrorKind::NotFound => None, Err(error) => return Err(error).context("reading route fragment"), @@ -6842,6 +6851,34 @@ mod tests { ) } + #[test] + fn an_empty_route_fragment_is_absence_not_a_membership() -> Result<()> { + let temp = tempfile::tempdir()?; + let config_path = temp.path().join("odin.conf"); + let driver = NginxRouteDriver::new(RouteBinding { + driver: RouteDriver::NginxStreamUdp, + route_id: "odin-rudp".into(), + stable_endpoint: "rudp://127.0.0.1:17872".into(), + private_host: "127.0.0.1".into(), + private_port_start: 27872, + private_port_end: 27879, + config_path: config_path.clone(), + reload_unit: "nginx.service".into(), + }); + assert_eq!(driver.current_configuration()?, None); + // What systemd leaves behind when it materializes the preflight's bind + // mount point on a target's first deployment. + std::fs::write(&config_path, b"")?; + assert_eq!( + driver.current_configuration()?, + None, + "an empty fragment must not read as an unadmitted incumbent" + ); + std::fs::write(&config_path, b"server {}")?; + assert_eq!(driver.current_configuration()?, Some(b"server {}".to_vec())); + Ok(()) + } + #[test] fn nginx_rudp_route_is_rendered_as_a_udp_stream_proxy() -> Result<()> { let driver = NginxRouteDriver::new(RouteBinding {