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 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-07 01:21:02 +02:00
commit fec605d7f8

View file

@ -4044,6 +4044,15 @@ impl NginxRouteDriver {
fn current_configuration(&self) -> Result<Option<Vec<u8>>> {
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 {