use super::super::ssh::{
RemoteSourceAuthorityLock, acquire_remote_source_authority_lock,
canonical_source_authority_root, run_offload_ssh_command_with_stdin,
validate_source_authority_intent,
};
use super::{PathTopologyPolicy, SyncClosurePlanEntry, WorkerConfig};
use anyhow::Context as _;
use sha2::{Digest as _, Sha256};
use std::time::Duration;
const REGISTRY: &str = "/tmp/rch-source-authority-locks/claims-v1";
const PATHS: &str = include_str!("source_namespace_paths.sh");
const ADMISSION: &str = include_str!("source_claim_admission.sh");
const CLAIMS: &str = include_str!("source_claim_registry.sh");
const CONTROL: &str = include_str!("source_namespace_control.sh");
const SOURCE_ADMISSION_WAIT: Duration = Duration::from_secs(60);
fn retryable_source_admission_error(error: &anyhow::Error) -> bool {
let detail = format!("{error:#}");
detail.contains("RCH: durable source ownership: unfinished overlapping source owner")
|| detail
.contains("RCH: durable source ownership: unfinished overlapping physical source owner")
}
pub(super) async fn acquire_source_authority_with_retry(
worker: &WorkerConfig,
roots: &[String],
mut source_pair: Option<&mut RemoteSourceAuthorityLock>,
identity: &str,
wait_timeout: Duration,
) -> anyhow::Result<RemoteSourceAuthorityLock> {
let deadline = tokio::time::Instant::now() + wait_timeout.min(SOURCE_ADMISSION_WAIT);
loop {
let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
if remaining.is_zero() {
anyhow::bail!(
"timed out waiting for overlapping source ownership on {}",
worker.id
);
}
match acquire_remote_source_authority_lock(
worker,
roots,
source_pair.as_deref_mut(),
identity,
remaining,
)
.await
{
Ok(guard) => return Ok(guard),
Err(error)
if retryable_source_admission_error(&error)
&& tokio::time::Instant::now() < deadline =>
{
let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
if remaining.is_zero() {
return Err(error);
}
tokio::time::sleep(Duration::from_secs(1).min(remaining)).await;
}
Err(error) => return Err(error),
}
}
}
pub(super) fn build_source_roots(
plan: &[SyncClosurePlanEntry],
pair_source: Option<&str>,
retained_source: Option<&str>,
freshness_control: Option<&str>,
target: Option<&str>,
) -> Vec<String> {
let mut roots = plan
.iter()
.map(|entry| entry.remote_root.as_str())
.chain(
[pair_source, retained_source, freshness_control, target]
.into_iter()
.flatten(),
)
.map(canonical_source_authority_root)
.collect::<Vec<_>>();
roots.sort();
roots.dedup();
roots
}
fn namespace_command(
policy: &PathTopologyPolicy,
identity: &str,
registry: &str,
) -> anyhow::Result<String> {
let canonical = policy
.canonical_root()
.to_str()
.context("worker canonical topology root is not UTF-8")?;
let alias = policy
.alias_root()
.to_str()
.context("worker alias topology root is not UTF-8")?;
let canonical = canonical_source_authority_root(canonical);
let alias = canonical_source_authority_root(alias);
let mut roots = vec![canonical.clone(), alias.clone()];
roots.sort();
roots.dedup();
validate_source_authority_intent(&roots, identity)?;
let body = format!("{}\n", roots.join("\n"));
let digest = Sha256::digest(body.as_bytes())
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>();
let quote = |value: &str| shell_escape::escape(value.into()).into_owned();
let paths = |mode: &str| {
format!(
"set -- {mode} {} {}\n{PATHS}",
quote(&canonical),
quote(&alias),
)
};
let registry_script = format!(
"{}\n{CLAIMS}",
rch_common::stale_target_reap::SOURCE_CLAIM_RECORD_HELPERS,
);
Ok(format!(
"set -eu\numask 077\nregistry={registry}\n\
[ ! -L \"$registry\" ]\nmkdir -p -- \"$registry\"\n\
for directory in \"$registry/released\" \"$registry/cancelled\" \"$registry/quarantine\"; do\n\
[ ! -L \"$directory\" ]\nmkdir -p -- \"$directory\"\ndone\n\
[ ! -L \"$registry/metadata.lock\" ]\nsync -f \"$registry\"\n\
rch_source_attempt() {{ (\nset -eu\n\
exec flock -x -- \"$registry/metadata.lock\" sh -c {script} \
rch-source-namespace \"$registry\" {identity} {digest} topology {check} {create} \
<<'RCH_NAMESPACE_SCOPE'\n{body}RCH_NAMESPACE_SCOPE\n\
); }}\nrch_admission_attempts=6\n{ADMISSION}",
registry = quote(registry),
script = quote(®istry_script),
identity = quote(identity),
check = quote(&paths("check")),
create = quote(&paths("create")),
))
}
pub(super) async fn initialize_worker_namespace(
worker: &WorkerConfig,
policy: &PathTopologyPolicy,
identity: &str,
) -> anyhow::Result<()> {
let script = namespace_command(policy, identity, REGISTRY)?;
let output = run_offload_ssh_command_with_stdin(
worker,
CONTROL,
script.as_bytes(),
Duration::from_secs(20),
)
.await?;
anyhow::ensure!(
output.status.success() && output.stdout.as_slice() == b"RCH_TOPOLOGY_OK\n",
"worker source namespace initialization refused on {} (status {:?}): {}",
worker.id,
output.status.code(),
String::from_utf8_lossy(&output.stderr).trim(),
);
Ok(())
}
#[cfg(test)]
mod tests {
use super::super::SyncClosureMode;
use super::*;
use std::path::PathBuf;
fn root(path: &str) -> SyncClosurePlanEntry {
SyncClosurePlanEntry {
local_root: PathBuf::from(path),
project_id: "test".to_owned(),
root_hash: "test".to_owned(),
remote_root: path.to_owned(),
is_primary: true,
mode: SyncClosureMode::Full,
}
}
#[test]
fn source_namespace_bases_are_not_inferred_as_owned_roots() {
let roots = build_source_roots(
&[root("/projects/a"), root("/projects/dependency")],
None,
None,
None,
Some("/pools/a"),
);
assert_eq!(roots, ["/pools/a", "/projects/a", "/projects/dependency"]);
let sibling = build_source_roots(&[root("/projects/b")], None, None, None, None);
assert_eq!(sibling, ["/projects/b"]);
}
#[test]
fn source_namespace_keeps_pair_retention_freshness_and_shared_targets() {
let roots = build_source_roots(
&[root("/staging/pair/repo"), root("/staging/pair/dep")],
Some("/staging/pair"),
Some("/staging/pair.retained-job"),
Some("/staging/pair.freshness"),
Some("/pools/shared"),
);
assert_eq!(
roots,
[
"/pools/shared",
"/staging/pair",
"/staging/pair.freshness",
"/staging/pair.retained-job",
"/staging/pair/dep",
"/staging/pair/repo",
]
);
}
#[test]
fn source_namespace_never_filters_a_genuinely_selected_base_or_dependency() {
let roots = build_source_roots(
&[
root("/projects/"),
root("/projects/repo"),
root("/projects/repo/"),
],
None,
None,
None,
Some("/projects/repo/.rch-target"),
);
assert_eq!(
roots,
["/projects", "/projects/repo", "/projects/repo/.rch-target"]
);
}
#[cfg(target_os = "linux")]
#[test]
fn source_namespace_production_command_initializes_without_a_claim() {
use std::io::Write as _;
use std::process::Stdio;
let temp = tempfile::tempdir().unwrap();
let canonical = temp.path().join("projects with ' quotes");
let alias = temp.path().join("alias");
let registry = temp.path().join("registry");
let policy = PathTopologyPolicy::new(canonical.clone(), alias.clone());
let command = namespace_command(&policy, "0123", registry.to_str().unwrap()).unwrap();
for _ in 0..2 {
let mut child = std::process::Command::new("sh")
.args(["-c", CONTROL])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
child
.stdin
.take()
.unwrap()
.write_all(command.as_bytes())
.unwrap();
let output = child.wait_with_output().unwrap();
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(output.stdout, b"RCH_TOPOLOGY_OK\n");
assert_eq!(std::fs::canonicalize(&alias).unwrap(), canonical);
assert!(std::fs::read_dir(®istry).unwrap().all(|entry| {
let path = entry.unwrap().path();
!matches!(
path.extension().and_then(|value| value.to_str()),
Some("claim" | "pending")
)
}));
}
}
#[test]
fn source_admission_retry_classifier_is_narrow() {
let lexical = anyhow::anyhow!(
"source-authority lock exited; stderr=RCH: durable source ownership: unfinished overlapping source owner"
);
let physical = anyhow::anyhow!(
"source-authority lock exited; stderr=RCH: durable source ownership: unfinished overlapping physical source owner"
);
assert!(retryable_source_admission_error(&lexical));
assert!(retryable_source_admission_error(&physical));
for terminal in [
"RCH: durable source ownership: invalid claim record",
"RCH: durable source ownership: source intent cancelled",
"RCH: durable source ownership: source identity already claimed",
"SSH command timed out",
] {
assert!(!retryable_source_admission_error(&anyhow::anyhow!(
"{terminal}"
)));
}
}
#[test]
fn source_namespace_rejects_bad_intents_before_remote_setup() {
let policy = PathTopologyPolicy::new("/projects".into(), "/dp".into());
assert!(namespace_command(&policy, "not-a-token", REGISTRY).is_err());
let bad = PathTopologyPolicy::new("/projects\n/injected".into(), "/dp".into());
assert!(namespace_command(&bad, "0123", REGISTRY).is_err());
}
}