perf(async): remove blocking std::process::Command from async paths
Every production process spawn reachable from a tokio worker now uses tokio::process: the install path's podman-port probe, the dependencies disk check, factory-reset restart, config host-IP detection, the orchestrator's host-facts helpers (resolve_dynamic_env and its call sites made async to carry it through), and AutoRuntime's podman/docker probes. The FIPS transport probe is the special case: is_available() is a sync trait method called from async route(), so instead of blocking ~50ms on systemctl per stale-cache hit it now serves the cached value and refreshes on a background thread (stale-while-revalidate) — bounded staleness, zero stalled workers. §C of the 1.8.0 hardening plan; container/transport/config/package suites green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
01cbec27ed
commit
4c75bb3d38
@@ -1440,7 +1440,7 @@ impl ProdContainerOrchestrator {
|
||||
.collect()
|
||||
};
|
||||
let mut report = ReconcileReport::default();
|
||||
let disk_gb = self.disk_gb();
|
||||
let disk_gb = self.disk_gb().await;
|
||||
// Register every candidate before the (sequential, possibly slow)
|
||||
// pass so the scanner overlays queued-but-down apps as Restarting
|
||||
// instead of Stopped. Each app is deregistered as its turn finishes,
|
||||
@@ -1620,7 +1620,7 @@ impl ProdContainerOrchestrator {
|
||||
}
|
||||
|
||||
let mut resolved_manifest = lm.manifest.clone();
|
||||
self.resolve_dynamic_env(&mut resolved_manifest)?;
|
||||
self.resolve_dynamic_env(&mut resolved_manifest).await?;
|
||||
let name = compute_container_name(&lm.manifest);
|
||||
|
||||
// An explicitly user-stopped app MUST stay stopped. The reconcile filter
|
||||
@@ -1970,7 +1970,7 @@ impl ProdContainerOrchestrator {
|
||||
async fn install_fresh(&self, lm: &LoadedManifest) -> Result<()> {
|
||||
self.ensure_app_secrets(&lm.manifest.app.id).await?;
|
||||
let mut resolved_manifest = lm.manifest.clone();
|
||||
self.resolve_dynamic_env(&mut resolved_manifest)?;
|
||||
self.resolve_dynamic_env(&mut resolved_manifest).await?;
|
||||
resolve_catalog_image(&mut resolved_manifest);
|
||||
|
||||
let resolved = resolved_manifest.app.container.resolve().ok_or_else(|| {
|
||||
@@ -2263,7 +2263,7 @@ impl ProdContainerOrchestrator {
|
||||
// Re-render the manifest with dynamic env baked in, then go
|
||||
// through the same install path a fresh install would.
|
||||
let mut resolved = lm.manifest.clone();
|
||||
self.resolve_dynamic_env(&mut resolved)?;
|
||||
self.resolve_dynamic_env(&mut resolved).await?;
|
||||
self.install_via_quadlet(&resolved, name)
|
||||
.await
|
||||
.with_context(|| format!("Phase 3.3: re-install {name} via Quadlet"))?;
|
||||
@@ -2329,7 +2329,7 @@ impl ProdContainerOrchestrator {
|
||||
let restart_required = quadlet::contains_stale_health_gate(&old_body);
|
||||
|
||||
let mut resolved = lm.manifest.clone();
|
||||
self.resolve_dynamic_env(&mut resolved)?;
|
||||
self.resolve_dynamic_env(&mut resolved).await?;
|
||||
// Same catalog/pinned-version image resolution the installer applies, so
|
||||
// the drift re-render doesn't revert a pinned version back to the
|
||||
// manifest's shipped `:latest` tag on the next reconcile tick.
|
||||
@@ -2404,7 +2404,7 @@ impl ProdContainerOrchestrator {
|
||||
|
||||
for dep in dependencies {
|
||||
let mut resolved = dep.manifest.clone();
|
||||
self.resolve_dynamic_env(&mut resolved)?;
|
||||
self.resolve_dynamic_env(&mut resolved).await?;
|
||||
let name = compute_container_name(&dep.manifest);
|
||||
if self.runtime.get_container_status(&name).await.is_err() {
|
||||
continue;
|
||||
@@ -2588,7 +2588,7 @@ impl ProdContainerOrchestrator {
|
||||
}
|
||||
.read("bitcoin-rpc-password")
|
||||
.context("lnd pre-start: read bitcoin RPC password")?;
|
||||
let bitcoin_host = self.bitcoin_host();
|
||||
let bitcoin_host = self.bitcoin_host().await;
|
||||
let outcome = lnd::ensure_config(&self.lnd_paths, &rpc_pass, &bitcoin_host)
|
||||
.await
|
||||
.context("lnd pre-start: ensure lnd.conf")?;
|
||||
@@ -2706,10 +2706,12 @@ impl ProdContainerOrchestrator {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
}
|
||||
|
||||
fn detect_host_facts(&self) -> HostFacts {
|
||||
let host_ip = Self::detect_host_ip().unwrap_or_else(|| "127.0.0.1".to_string());
|
||||
let host_mdns = Self::detect_host_mdns();
|
||||
let disk_gb = self.disk_gb();
|
||||
async fn detect_host_facts(&self) -> HostFacts {
|
||||
let host_ip = Self::detect_host_ip()
|
||||
.await
|
||||
.unwrap_or_else(|| "127.0.0.1".to_string());
|
||||
let host_mdns = Self::detect_host_mdns().await;
|
||||
let disk_gb = self.disk_gb().await;
|
||||
HostFacts {
|
||||
host_ip,
|
||||
host_mdns,
|
||||
@@ -2723,9 +2725,8 @@ impl ProdContainerOrchestrator {
|
||||
|
||||
/// Container name of the running Bitcoin node (`bitcoin-knots` or
|
||||
/// `bitcoin-core`) for the `{{BITCOIN_HOST}}` derived-env placeholder.
|
||||
/// Synchronous `podman ps` to match the surrounding host-fact detection;
|
||||
/// defaults to `bitcoin-knots` when none is running (B12).
|
||||
fn bitcoin_host(&self) -> String {
|
||||
/// Defaults to `bitcoin-knots` when none is running (B12).
|
||||
async fn bitcoin_host(&self) -> String {
|
||||
#[cfg(test)]
|
||||
if let Some(host) = &self.test_bitcoin_host {
|
||||
return host.clone();
|
||||
@@ -2733,9 +2734,10 @@ impl ProdContainerOrchestrator {
|
||||
// Mirrors api::rpc::package::dependencies (the legacy install path);
|
||||
// both Bitcoin node variants are reachable on archy-net by name.
|
||||
const BITCOIN_NAMES: &[&str] = &["bitcoin-knots", "bitcoin-core", "bitcoin"];
|
||||
let names = Command::new("podman")
|
||||
let names = tokio::process::Command::new("podman")
|
||||
.args(["ps", "--format", "{{.Names}}"])
|
||||
.output()
|
||||
.await
|
||||
.ok()
|
||||
.filter(|o| o.status.success())
|
||||
.map(|o| String::from_utf8_lossy(&o.stdout).into_owned())
|
||||
@@ -2753,8 +2755,12 @@ impl ProdContainerOrchestrator {
|
||||
self.test_bitcoin_host = Some(host.to_string());
|
||||
}
|
||||
|
||||
fn detect_host_ip() -> Option<String> {
|
||||
let output = Command::new("hostname").arg("-I").output().ok()?;
|
||||
async fn detect_host_ip() -> Option<String> {
|
||||
let output = tokio::process::Command::new("hostname")
|
||||
.arg("-I")
|
||||
.output()
|
||||
.await
|
||||
.ok()?;
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
@@ -2762,9 +2768,10 @@ impl ProdContainerOrchestrator {
|
||||
stdout.split_whitespace().next().map(|s| s.to_string())
|
||||
}
|
||||
|
||||
fn detect_host_mdns() -> String {
|
||||
let hostname = Command::new("hostname")
|
||||
async fn detect_host_mdns() -> String {
|
||||
let hostname = tokio::process::Command::new("hostname")
|
||||
.output()
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|o| {
|
||||
if o.status.success() {
|
||||
@@ -2782,13 +2789,18 @@ impl ProdContainerOrchestrator {
|
||||
}
|
||||
}
|
||||
|
||||
fn detect_disk_gb() -> u64 {
|
||||
async fn detect_disk_gb() -> u64 {
|
||||
let target = if Path::new("/var/lib/archipelago").exists() {
|
||||
"/var/lib/archipelago"
|
||||
} else {
|
||||
"/"
|
||||
};
|
||||
let output = match Command::new("df").arg("-k").arg(target).output() {
|
||||
let output = match tokio::process::Command::new("df")
|
||||
.arg("-k")
|
||||
.arg(target)
|
||||
.output()
|
||||
.await
|
||||
{
|
||||
Ok(o) if o.status.success() => o,
|
||||
_ => return 0,
|
||||
};
|
||||
@@ -2808,12 +2820,12 @@ impl ProdContainerOrchestrator {
|
||||
kb / 1_000_000
|
||||
}
|
||||
|
||||
fn disk_gb(&self) -> u64 {
|
||||
async fn disk_gb(&self) -> u64 {
|
||||
#[cfg(test)]
|
||||
if let Some(disk_gb) = self.test_disk_gb {
|
||||
return disk_gb;
|
||||
}
|
||||
Self::detect_disk_gb()
|
||||
Self::detect_disk_gb().await
|
||||
}
|
||||
|
||||
/// Ensure app-specific secrets exist *before* env resolution. The Bitcoin
|
||||
@@ -2838,14 +2850,14 @@ impl ProdContainerOrchestrator {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resolve_dynamic_env(&self, manifest: &mut AppManifest) -> Result<()> {
|
||||
async fn resolve_dynamic_env(&self, manifest: &mut AppManifest) -> Result<()> {
|
||||
// Materialise any manifest-declared generated secrets before they're
|
||||
// read below. This is the single chokepoint every install/reconcile
|
||||
// path funnels through, so an app's secrets exist by the time its
|
||||
// `secret_env` resolves — no per-app code, no host provisioning.
|
||||
crate::container::secrets::ensure_generated_secrets(&self.secrets_dir, manifest)?;
|
||||
|
||||
let mut facts = self.detect_host_facts();
|
||||
let mut facts = self.detect_host_facts().await;
|
||||
// Only pay the podman cost to detect Knots-vs-Core when this manifest
|
||||
// actually templates the Bitcoin node into its env (mempool — B12).
|
||||
if manifest
|
||||
@@ -2855,7 +2867,7 @@ impl ProdContainerOrchestrator {
|
||||
.iter()
|
||||
.any(|e| e.template.contains("{{BITCOIN_HOST}}"))
|
||||
{
|
||||
facts.bitcoin_host = self.bitcoin_host();
|
||||
facts.bitcoin_host = self.bitcoin_host().await;
|
||||
}
|
||||
let mut env = manifest.app.environment.clone();
|
||||
env.extend(manifest.app.container.resolve_derived_env(&facts));
|
||||
@@ -3208,7 +3220,7 @@ impl ProdContainerOrchestrator {
|
||||
) -> Result<String> {
|
||||
let mut out = content.to_string();
|
||||
if out.contains("{{HOST_IP}}") || out.contains("{{HOST_MDNS}}") {
|
||||
let facts = self.detect_host_facts();
|
||||
let facts = self.detect_host_facts().await;
|
||||
out = out
|
||||
.replace("{{HOST_IP}}", &facts.host_ip)
|
||||
.replace("{{HOST_MDNS}}", &facts.host_mdns);
|
||||
@@ -3297,7 +3309,7 @@ impl ProdContainerOrchestrator {
|
||||
/// however the box is reached locally. (Generalised from the old per-app
|
||||
/// netbird TLS helper, deleted in #20 ph4: rsa:2048, 10-year, no per-app Rust.)
|
||||
async fn ensure_manifest_certs(&self, manifest: &AppManifest) -> Result<()> {
|
||||
let facts = self.detect_host_facts();
|
||||
let facts = self.detect_host_facts().await;
|
||||
let render = |s: &str| {
|
||||
s.replace("{{HOST_IP}}", &facts.host_ip)
|
||||
.replace("{{HOST_MDNS}}", &facts.host_mdns)
|
||||
@@ -3594,7 +3606,7 @@ impl ContainerOrchestrator for ProdContainerOrchestrator {
|
||||
self.ensure_app_secrets(app_id).await?;
|
||||
let name = compute_container_name(&lm.manifest);
|
||||
let mut resolved_manifest = lm.manifest.clone();
|
||||
self.resolve_dynamic_env(&mut resolved_manifest)?;
|
||||
self.resolve_dynamic_env(&mut resolved_manifest).await?;
|
||||
|
||||
let service = format!("{name}.service");
|
||||
if self.quadlet_unit_exists(&name).await? {
|
||||
@@ -4328,7 +4340,7 @@ app:
|
||||
orch.set_bitcoin_host_for_test(node);
|
||||
|
||||
let mut manifest = AppManifest::parse(yaml).unwrap();
|
||||
orch.resolve_dynamic_env(&mut manifest).unwrap();
|
||||
orch.resolve_dynamic_env(&mut manifest).await.unwrap();
|
||||
|
||||
assert!(
|
||||
manifest
|
||||
|
||||
Reference in New Issue
Block a user