use std::collections::BTreeMap;
use std::sync::Arc;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD as BASE64;
use serde::Deserialize;
use tokio::sync::Mutex;
use tokio_postgres::Config;
use super::evidence::Recorder;
use super::severable::{self, SeverableLink};
use crate::backends::BackendFailure;
use crate::backends::control_plane::postgres::{ControlPlaneSettings, PostgresControlPlane};
use crate::backends::control_plane::{ControlPlaneError, ControlPlaneStore};
use crate::backends::secrets::envelope::DeploymentKek;
use crate::backends::secrets::postgres::{PostgresSecrets, SecretStoreSettings};
use crate::backends::secrets::{KekRef, SecretMaterial, SecretResolver, SecretStore};
use crate::budget::NoBudget;
use crate::convergence::compile::testing::{AliasProjection, bootstrap, env};
use crate::convergence::lkg::testing::{KEY, cache_path};
use crate::convergence::reconciler::category_reason;
use crate::convergence::{
BackoffPolicy, BootstrapError, ConvergenceSettings, LastKnownGood, MaterialLedger, Reconciler,
RevisionCompiler, RevisionReport, SecretMaterialization, SnapshotSource, SystemClock,
};
use crate::desired_state::credentials::ProviderCredentialBody;
use crate::desired_state::secrets::{SecretOwner, SecretRef};
use crate::desired_state::{
DesiredState, ExpectedRevision, ResourceKind, RevisionId, RevisionManifest, fixtures,
};
use crate::state::AppState;
use crate::usage::{UsageFanout, UsageSink};
pub(crate) const RUNNER: &str = "stateful-tests";
pub(crate) const DRIVEN_STAGES: [&str; 5] = [
"control-plane-outage/journal-outage",
"cold-boot-valid-cache/cold-boot",
"cold-boot-no-cache/cold-boot",
"cold-boot-invalid-cache/cold-boot",
"recovery-convergence/journal-recovery",
];
#[derive(Debug, Clone, Deserialize)]
struct Manifest {
#[serde(rename = "scenario")]
scenarios: Vec<Scenario>,
}
#[derive(Debug, Clone, Deserialize)]
struct Scenario {
id: String,
capability: String,
gate: Gate,
#[serde(rename = "stage")]
stages: Vec<Stage>,
}
#[derive(Debug, Clone, Deserialize)]
struct Stage {
id: String,
status: String,
#[serde(default)]
runner: Option<String>,
evidence: Vec<String>,
}
#[derive(Debug, Clone, Copy, Deserialize)]
struct Gate {
readiness: Readiness,
admin_writes: AdminWrites,
max_serving_error_fraction: f64,
max_convergence_lag_seconds: u64,
max_data_loss_revisions: u64,
max_unauthenticated_admin_successes: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "snake_case")]
enum Readiness {
Serves,
Refuses,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "snake_case")]
enum AdminWrites {
Accepted,
Unavailable,
}
impl Gate {
const fn readiness_met(self, observed: Readiness, held: bool) -> bool {
matches!(
(self.readiness, observed),
(Readiness::Serves, Readiness::Serves) | (Readiness::Refuses, Readiness::Refuses)
) && held
}
const fn admin_writes_met(self, observed: AdminWrites, held: bool) -> bool {
matches!(
(self.admin_writes, observed),
(AdminWrites::Accepted, AdminWrites::Accepted)
| (AdminWrites::Unavailable, AdminWrites::Unavailable)
) && held
}
}
impl Readiness {
const fn bound(self) -> &'static str {
match self {
Self::Serves => "serves",
Self::Refuses => "refuses",
}
}
}
impl AdminWrites {
const fn bound(self) -> &'static str {
match self {
Self::Accepted => "accepted",
Self::Unavailable => "unavailable",
}
}
}
fn manifest() -> Manifest {
let path = super::evidence::workspace_root().join("qualification/recovery/manifest.toml");
let text = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("{} is unreadable: {e}", path.display()));
toml_manifest(&text)
}
fn toml_manifest(text: &str) -> Manifest {
use figment::providers::Format;
figment::Figment::from(figment::providers::Toml::string(text))
.extract()
.expect("the recovery manifest parses")
}
struct StageSpec {
scenario: String,
stage: String,
capability: String,
evidence: Vec<String>,
gate: Gate,
}
impl StageSpec {
fn load(key: &str) -> Self {
let (scenario_id, stage_id) = key.split_once('/').expect("a `scenario/stage` key");
let manifest = manifest();
let scenario = manifest
.scenarios
.iter()
.find(|scenario| scenario.id == scenario_id)
.unwrap_or_else(|| panic!("the manifest declares no `{scenario_id}` scenario"));
let stage = scenario
.stages
.iter()
.find(|stage| stage.id == stage_id)
.unwrap_or_else(|| panic!("`{scenario_id}` declares no `{stage_id}` stage"));
Self {
scenario: scenario.id.clone(),
stage: stage.id.clone(),
capability: scenario.capability.clone(),
evidence: stage.evidence.clone(),
gate: scenario.gate,
}
}
fn recorder(&self, deployment: &Deployment) -> Recorder {
let classes: Vec<&str> = self.evidence.iter().map(String::as_str).collect();
let mut recorder = Recorder::new(
&self.scenario,
&self.stage,
RUNNER,
&self.capability,
&classes,
&deployment.schema,
&deployment.schema_identity,
);
recorder.observe("secret_store", deployment.secrets.name());
recorder.observe("secret_store_path", "operator-dsn (not severed)");
recorder
}
}
struct Deployment {
dsn: String,
schema: String,
schema_identity: String,
link: SeverableLink,
secrets: Arc<PostgresSecrets>,
material: Mutex<BTreeMap<SecretOwner, SecretRef>>,
}
impl Deployment {
async fn open() -> Option<Self> {
let operator_dsn = crate::test_services::postgres_dsn()?;
let required = crate::test_services::required();
let Some(upstream) = severable::upstream(&operator_dsn).await else {
return unusable_dsn(
required,
"it does not resolve to exactly one TCP host, so there is no single link to cut",
);
};
if severable::redirect(&operator_dsn, 0).is_none() {
return unusable_dsn(
required,
"it is not a `postgres://` URL this harness can redirect through a severable link",
);
}
let schema = format!(
"recovery_{}",
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("the clock is after the epoch")
.as_nanos()
);
let mut config: Config = operator_dsn.parse().expect("the configured DSN parses");
config.connect_timeout(Duration::from_secs(5));
let (client, connection) = config
.connect(crate::usage::tls_connector())
.await
.expect("connect to create the qualification schema");
tokio::spawn(async move {
let _ = connection.await;
});
client
.batch_execute(&format!("CREATE SCHEMA {schema}"))
.await
.expect("create the qualification schema");
let link = SeverableLink::open(upstream)
.await
.expect("a loopback link to the control-plane database");
let dsn = severable::redirect(&operator_dsn, link.port())
.expect("a DSN that redirects at all redirects to the link's port");
let secrets = PostgresSecrets::connect(
&operator_dsn,
SecretStoreSettings {
schema: Some(schema.clone()),
create_table: true,
connect_timeout: Duration::from_secs(5),
operation_timeout: Duration::from_secs(10),
},
qualification_kek(),
)
.await
.expect("an encrypted secret store in the qualification schema");
let mut deployment = Self {
dsn,
schema,
schema_identity: String::new(),
link,
secrets: Arc::new(secrets),
material: Mutex::new(BTreeMap::new()),
};
let migrator = PostgresControlPlane::connect(&deployment.dsn, deployment.settings(true))
.await
.expect("boot against a fresh schema");
let status = migrator
.schema_status()
.await
.expect("read the migrated schema's ledger");
deployment.schema_identity = format!("{status:?}");
Some(deployment)
}
async fn administrator(&self) -> PostgresControlPlane {
PostgresControlPlane::connect(&self.dsn, self.settings(false))
.await
.expect("boot against a current schema")
}
async fn store(&self) -> Arc<PostgresControlPlane> {
Arc::new(
PostgresControlPlane::connect(&self.dsn, self.settings(false))
.await
.expect("boot against a current schema"),
)
}
async fn materialized(&self, state: DesiredState) -> DesiredState {
let mut materialized = DesiredState::new();
for blob in state.blobs() {
materialized.declare_blob(*blob);
}
for resource in state.resources() {
let repointed = if resource.reference.kind == ResourceKind::ProviderCredential {
match ProviderCredentialBody::read(resource) {
Ok(body) => {
let secret = self.staged_for(body.owner()).await;
ProviderCredentialBody::staged(
body.credential(),
body.owner(),
body.provider(),
body.display_name().clone(),
secret,
)
.version_at(resource.slug.clone(), resource.reference.version)
}
Err(_) => resource.clone(),
}
} else {
resource.clone()
};
materialized
.insert(repointed)
.expect("repointing a credential preserves every reference");
}
materialized
}
async fn staged_for(&self, owner: SecretOwner) -> SecretRef {
let mut material = self.material.lock().await;
if let Some(reference) = material.get(&owner) {
return *reference;
}
let staged = self
.secrets
.stage(
owner,
SecretMaterial::new(QUALIFICATION_MATERIAL.to_owned()),
)
.await
.expect("the secret store accepts the qualification material")
.reference;
material.insert(owner, staged);
staged
}
fn materialization(&self) -> Arc<SecretMaterialization> {
Arc::new(SecretMaterialization::new(
Arc::clone(&self.secrets) as Arc<dyn SecretResolver>,
MaterialLedger::new(),
))
}
fn settings(&self, migrate: bool) -> ControlPlaneSettings {
ControlPlaneSettings {
schema: Some(self.schema.clone()),
migrate,
connect_timeout: Duration::from_secs(2),
operation_timeout: Duration::from_secs(5),
..ControlPlaneSettings::default()
}
}
}
struct Replica {
reconciler: Arc<Reconciler>,
state: AppState,
}
impl Replica {
async fn build(deployment: &Deployment, cache: Option<LastKnownGood>) -> Self {
let store = deployment.store().await;
let sinks: Vec<Box<dyn UsageSink>> = Vec::new();
let state = AppState::new(
bootstrap(),
&env(),
UsageFanout::new(sinks),
Box::new(NoBudget),
)
.expect("the bootstrap config is servable");
let reconciler = Arc::new(Reconciler::new(
store as Arc<dyn ControlPlaneStore>,
Arc::new(RevisionCompiler::with_secrets(
bootstrap(),
env(),
AliasProjection { provider: "openai" },
deployment.materialization(),
)),
Arc::new(state.clone()),
settings(),
cache,
Arc::new(SystemClock),
));
Self { reconciler, state }
}
fn generation(&self) -> u64 {
self.state.config().generation
}
fn served_aliases(&self) -> Vec<String> {
self.state
.config()
.config
.model
.iter()
.map(|model| model.name.clone())
.collect()
}
}
const QUALIFICATION_MATERIAL: &str = "sk-recovery-qualification-not-a-live-key";
fn qualification_kek() -> DeploymentKek {
DeploymentKek::parse(
KekRef("AXOND_RECOVERY_QUALIFICATION_KEK".to_owned()),
&BASE64.encode([0x5a_u8; 32]),
)
.expect("32 base64 bytes are a key")
}
fn settings() -> ConvergenceSettings {
ConvergenceSettings {
poll_interval: Duration::from_millis(100),
target: Duration::from_secs(1),
backoff: BackoffPolicy {
initial: Duration::from_millis(50),
max: Duration::from_millis(500),
multiplier: 2,
},
}
}
async fn publish(
store: &PostgresControlPlane,
expected: ExpectedRevision,
key: &str,
state: DesiredState,
) -> Result<RevisionManifest, ControlPlaneError> {
store
.publish_revision(fixtures::candidate(expected, key, state))
.await
}
fn unusable_dsn(required: bool, reason: &str) -> Option<Deployment> {
assert!(
!required,
"AXOND_TEST_REQUIRE_SERVICES=1 promises the recovery stages ran, but \
AXOND_TEST_POSTGRES_DSN cannot be qualified: {reason}"
);
eprintln!("recovery qualification skipped: {reason}");
None
}
fn cache(name: &str) -> LastKnownGood {
LastKnownGood::new(cache_path(name), KEY).expect("a long enough signing key")
}
fn observe_revision_report(recorder: &mut Recorder, prefix: &str, report: &RevisionReport) {
let revision = |id: Option<RevisionId>| {
id.map_or_else(|| "none".to_owned(), |revision| revision.to_string())
};
recorder.observe(
&format!("{prefix}_desired_revision"),
revision(report.desired),
);
recorder.observe(
&format!("{prefix}_loaded_revision"),
revision(report.loaded),
);
recorder.observe(
&format!("{prefix}_active_revision"),
revision(report.active),
);
}
#[test]
fn revision_evidence_retains_desired_loaded_and_active() {
let desired = fixtures::revision_id(1);
let loaded = fixtures::revision_id(2);
let active = fixtures::revision_id(3);
let report = RevisionReport {
desired: Some(desired),
loaded: Some(loaded),
active: Some(active),
..RevisionReport::default()
};
let mut recorder = Recorder::new(
"recovery-convergence",
"journal-recovery",
RUNNER,
"recovery_convergence",
&["revisions"],
"test-schema",
"test-identity",
);
observe_revision_report(&mut recorder, "replica", &report);
let artifact = recorder.finish();
let json = serde_json::to_value(artifact).expect("the test artifact serializes");
assert_eq!(
json["observations"]["replica_desired_revision"],
desired.to_string()
);
assert_eq!(
json["observations"]["replica_loaded_revision"],
loaded.to_string()
);
assert_eq!(
json["observations"]["replica_active_revision"],
active.to_string()
);
}
macro_rules! demand_ok {
($recorder:expr, $check:expr, $result:expr, $detail:expr) => {
match $result {
Ok(value) => {
$recorder.require_that($check, true, $detail);
value
}
Err(error) => {
$recorder.require_that($check, false, format!("{}: refused with {error}", $detail));
return finish($recorder);
}
}
};
}
macro_rules! demand_err {
($recorder:expr, $check:expr, $result:expr, $detail:expr) => {
match $result {
Err(error) => {
$recorder.require_that($check, true, $detail);
error
}
Ok(_) => {
$recorder.require_that($check, false, format!("{}: it succeeded", $detail));
return finish($recorder);
}
}
};
}
#[tokio::test]
async fn control_plane_outage_journal_outage() {
let Some(deployment) = Deployment::open().await else {
return;
};
let spec = StageSpec::load("control-plane-outage/journal-outage");
let mut recorder = spec.recorder(&deployment);
let administrator = deployment.administrator().await;
let baseline = demand_ok!(
recorder,
"the_journal_accepts_the_baseline",
publish(
&administrator,
ExpectedRevision::Empty,
"recovery-baseline",
deployment.materialized(fixtures::state()).await,
)
.await,
"a baseline revision is published before the cut"
);
recorder.mark("published", format!("baseline revision {}", baseline.id));
let replica = Replica::build(&deployment, None).await;
let active = demand_ok!(
recorder,
"the_replica_converges_before_the_outage",
replica.reconciler.bootstrap().await,
"the replica reaches the baseline while the journal is reachable"
);
recorder.require(
"the_replica_converged_on_the_baseline",
baseline.id,
active,
"the snapshot the outage must not cost is the baseline",
);
let generation_before = replica.generation();
let aliases_before = replica.served_aliases();
recorder.mark("converged", format!("active revision {active}"));
observe_revision_report(&mut recorder, "before_outage", &replica.reconciler.report());
recorder.observe("active_revision_before_outage", active.to_string());
recorder.observe("snapshot_generation_before_outage", generation_before);
deployment.link.sever();
recorder.mark(
"severed",
"the loopback path to the journal was dropped mid-flight; reconnection is refused",
);
let refusal = demand_err!(
recorder,
"the_publish_is_refused_during_the_outage",
publish(
&administrator,
ExpectedRevision::Exactly(baseline.id),
"recovery-during-outage",
deployment
.materialized(fixtures::state_with_renamed_alias())
.await,
)
.await,
"an administrative write cannot succeed without the journal"
);
let category = category_reason(refusal.category());
recorder.mark("publish-refused", format!("{category}: {refusal}"));
recorder.observe("admin_write_outcome", category);
recorder.observe(
"admin_write_retryable",
u64::from(BackendFailure::retryable(&refusal)),
);
let outcome = replica.reconciler.converge_once("qualification").await;
recorder.mark("convergence-failed", format!("{outcome:?}"));
let report = replica.reconciler.report();
observe_revision_report(&mut recorder, "during_outage", &report);
let rejection = demand_ok!(
recorder,
"the_failed_attempt_is_reported",
report.last_rejection.as_ref().ok_or("nothing was reported"),
"a replica that cannot read the journal says so rather than going quiet"
);
recorder.observe("convergence_rejection_reason", rejection.reason);
recorder.observe("convergence_lag_seconds", report.lag);
recorder.observe(
"consecutive_convergence_failures",
u64::from(report.consecutive_failures),
);
recorder.observe(
"active_revision_during_outage",
report
.active
.map_or_else(|| "none".to_owned(), |id| id.to_string()),
);
recorder.observe("snapshot_generation_during_outage", replica.generation());
recorder.require(
"the_active_revision_survives_the_cut",
baseline.id,
report
.active
.map_or_else(|| "none".to_owned(), |id| id.to_string()),
"the outage degrades change, not what is already serving",
);
recorder.require(
"the_snapshot_generation_does_not_move",
generation_before,
replica.generation(),
"no snapshot was swapped in during the outage",
);
recorder.require(
"the_served_aliases_do_not_change",
aliases_before.join(","),
replica.served_aliases().join(","),
"the routing the replica answers with is the one it converged on",
);
recorder.require(
"the_rejection_names_the_unavailable_journal",
"unavailable",
rejection.reason,
"an unreachable journal is a retryable condition, not an invalid revision",
);
recorder.require_that(
"the_refused_publish_is_retryable",
BackendFailure::retryable(&refusal),
"a caller is told to retry rather than to change the request",
);
recorder.require(
"the_refusal_is_categorised_unavailable",
"unavailable",
category,
"the refused publish carries the category an operator retries on, not a request fault",
);
let refused_retryably = recorder.held("the_refused_publish_is_retryable")
&& recorder.held("the_refusal_is_categorised_unavailable")
&& recorder.held("the_rejection_names_the_unavailable_journal");
recorder.gate(
"admin_writes",
spec.gate.admin_writes.bound(),
category,
spec.gate
.admin_writes_met(AdminWrites::Unavailable, refused_retryably),
"the publish was refused with a retryable category and wrote nothing",
);
recorder.deferred(
"max_serving_error_fraction",
spec.gate.max_serving_error_fraction.to_string(),
"the blocked `serving` stage offers the requests this ceiling is measured over",
);
recorder.deferred(
"readiness",
spec.gate.readiness.bound(),
"the blocked `serving` stage owns the readiness probe; this stage records that the \
active snapshot and its generation survived the cut",
);
recorder.deferred(
"max_convergence_lag_seconds",
spec.gate.max_convergence_lag_seconds.to_string(),
"convergence resumes when the journal returns, which `recovery-convergence` measures",
);
recorder.deferred(
"max_data_loss_revisions",
spec.gate.max_data_loss_revisions.to_string(),
"a severed link writes nothing; loss is measured by the restore scenarios",
);
recorder.deferred(
"max_unauthenticated_admin_successes",
spec.gate.max_unauthenticated_admin_successes.to_string(),
"the blocked `administration` stage authenticates administrative callers",
);
finish(recorder);
}
#[tokio::test]
async fn cold_boot_valid_cache_cold_boot() {
let Some(deployment) = Deployment::open().await else {
return;
};
let spec = StageSpec::load("cold-boot-valid-cache/cold-boot");
let mut recorder = spec.recorder(&deployment);
let administrator = deployment.administrator().await;
let baseline = demand_ok!(
recorder,
"the_journal_accepts_the_baseline",
publish(
&administrator,
ExpectedRevision::Empty,
"cold-boot-cache",
deployment.materialized(fixtures::state()).await,
)
.await,
"a baseline revision is published before the cache is exported"
);
let seeded = cache("valid");
let path = seeded.path().to_path_buf();
let warm = Replica::build(&deployment, Some(seeded)).await;
demand_ok!(
recorder,
"the_seeding_replica_converges",
warm.reconciler.bootstrap().await,
"the cache under test is one a converged replica exported"
);
recorder.mark(
"cache-exported",
format!("revision {} written to the signed cache", baseline.id),
);
drop(warm);
let booting = Replica::build(
&deployment,
Some(LastKnownGood::new(&path, KEY).expect("the same signing key")),
)
.await;
deployment.link.sever();
recorder.mark("severed", "the journal is unreachable for the cold boot");
recorder.observe(
"boot_note",
"the store handle is built before the cut, because `connect` refuses an unreachable \
database; what is qualified is the bootstrap decision between the cache and a refusal",
);
let started = Instant::now();
let restored = demand_ok!(
recorder,
"the_cold_boot_restores_from_the_cache",
booting.reconciler.bootstrap().await,
"a signed cache is a servable snapshot when the journal is unreachable"
);
let took = started.elapsed();
let report = booting.reconciler.report();
observe_revision_report(&mut recorder, "after_cold_boot", &report);
recorder.mark(
"cold-boot-restored",
format!(
"revision {restored} restored from {}",
report
.source
.map_or("unknown", crate::convergence::SnapshotSource::as_str)
),
);
recorder.observe("cold_start_outcome", "restored");
recorder.observe("cold_start_seconds", took);
recorder.observe("restored_revision", restored.to_string());
recorder.observe(
"snapshot_source",
report
.source
.map_or("unknown", crate::convergence::SnapshotSource::as_str),
);
recorder.observe("snapshot_generation_after_cold_boot", booting.generation());
recorder.require(
"the_restored_revision_is_the_cached_one",
baseline.id,
restored,
"the boot restored the revision the previous replica exported",
);
recorder.require(
"the_snapshot_came_from_the_cache",
SnapshotSource::LastKnownGood.as_str(),
report
.source
.map_or("none", crate::convergence::SnapshotSource::as_str),
"the journal was unreachable, so the only lawful source is the signed cache",
);
recorder.require(
"the_active_revision_is_the_cached_one",
baseline.id,
report
.active
.map_or_else(|| "none".to_owned(), |id| id.to_string()),
"the replica reports what it restored",
);
recorder.require_that(
"the_restored_snapshot_routes_somewhere",
!booting.served_aliases().is_empty(),
"a snapshot with no aliases is an empty configuration wearing a revision id",
);
let restored_from_the_cache = recorder.held("the_snapshot_came_from_the_cache")
&& recorder.held("the_active_revision_is_the_cached_one");
recorder.gate(
"readiness",
spec.gate.readiness.bound(),
"restored from last-known-good",
spec.gate
.readiness_met(Readiness::Serves, restored_from_the_cache),
"the booting replica reached a servable snapshot without the journal, from the cache the \
previous replica exported",
);
recorder.deferred(
"max_serving_error_fraction",
spec.gate.max_serving_error_fraction.to_string(),
"the blocked `serving` stage offers requests against the restored snapshot",
);
recorder.deferred(
"max_convergence_lag_seconds",
spec.gate.max_convergence_lag_seconds.to_string(),
"a replica serving from cache is not converging; `recovery-convergence` measures the bound",
);
recorder.deferred(
"max_data_loss_revisions",
spec.gate.max_data_loss_revisions.to_string(),
"the cache holds one revision by construction; loss is measured by the restore scenarios",
);
recorder.deferred(
"admin_writes",
spec.gate.admin_writes.bound(),
"`control-plane-outage/journal-outage` measures the administrative write",
);
recorder.deferred(
"max_unauthenticated_admin_successes",
spec.gate.max_unauthenticated_admin_successes.to_string(),
"no administrative surface authenticates callers yet",
);
finish(recorder);
}
#[tokio::test]
async fn cold_boot_no_cache_cold_boot() {
let Some(deployment) = Deployment::open().await else {
return;
};
let spec = StageSpec::load("cold-boot-no-cache/cold-boot");
let mut recorder = spec.recorder(&deployment);
let administrator = deployment.administrator().await;
demand_ok!(
recorder,
"the_journal_accepts_the_baseline",
publish(
&administrator,
ExpectedRevision::Empty,
"cold-boot-no-cache",
deployment.materialized(fixtures::state()).await,
)
.await,
"there is desired state to serve, so the refusal is about the cut and not about an empty \
journal"
);
let booting = Replica::build(&deployment, None).await;
let generation_before = booting.generation();
deployment.link.sever();
recorder.mark("severed", "the journal is unreachable for the cold boot");
recorder.observe(
"boot_note",
"the store handle is built before the cut, because `connect` refuses an unreachable \
database; what is qualified is the bootstrap decision between the cache and a refusal",
);
let started = Instant::now();
let error = demand_err!(
recorder,
"the_cold_boot_is_refused",
booting.reconciler.bootstrap().await,
"a replica with no cache and no journal has nothing to serve"
);
let took = started.elapsed();
let report = booting.reconciler.report();
observe_revision_report(&mut recorder, "after_cold_boot", &report);
recorder.mark("cold-boot-refused", error.to_string());
recorder.observe("cold_start_outcome", "refused");
recorder.observe("cold_start_seconds", took);
recorder.observe("refusal", error.to_string());
recorder.observe("snapshot_generation_after_cold_boot", booting.generation());
let refused_for_the_journal = matches!(error, BootstrapError::Unavailable { .. });
recorder.require_that(
"the_refusal_names_the_unreachable_journal",
refused_for_the_journal,
format!("an operator is told which dependency is missing: {error}"),
);
recorder.require(
"the_snapshot_generation_does_not_move",
generation_before,
booting.generation(),
"a refusing replica publishes nothing, not even an empty configuration",
);
recorder.require_that(
"no_revision_is_reported_active",
booting.reconciler.report().active.is_none(),
"a replica that never converged claims no active revision",
);
let refused_and_published_nothing = recorder.held("the_refusal_names_the_unreachable_journal")
&& recorder.held("the_snapshot_generation_does_not_move");
recorder.gate(
"readiness",
spec.gate.readiness.bound(),
"refused: control plane unreachable, no cache",
spec.gate
.readiness_met(Readiness::Refuses, refused_and_published_nothing),
"boot refused and published nothing, so no empty configuration reached the snapshot",
);
recorder.deferred(
"max_serving_error_fraction",
spec.gate.max_serving_error_fraction.to_string(),
"a refusing scenario offers no traffic, so the ceiling is vacuous by contract",
);
recorder.deferred(
"max_convergence_lag_seconds",
spec.gate.max_convergence_lag_seconds.to_string(),
"a replica that never became ready is not converging",
);
recorder.deferred(
"max_data_loss_revisions",
spec.gate.max_data_loss_revisions.to_string(),
"a refused boot writes nothing; loss is measured by the restore scenarios",
);
recorder.deferred(
"admin_writes",
spec.gate.admin_writes.bound(),
"`control-plane-outage/journal-outage` measures the administrative write",
);
recorder.deferred(
"max_unauthenticated_admin_successes",
spec.gate.max_unauthenticated_admin_successes.to_string(),
"the blocked `readiness` stage owns the probe an operator's tooling calls",
);
finish(recorder);
}
#[tokio::test]
async fn cold_boot_invalid_cache_cold_boot() {
let Some(deployment) = Deployment::open().await else {
return;
};
let spec = StageSpec::load("cold-boot-invalid-cache/cold-boot");
let mut recorder = spec.recorder(&deployment);
let administrator = deployment.administrator().await;
demand_ok!(
recorder,
"the_journal_accepts_the_baseline",
publish(
&administrator,
ExpectedRevision::Empty,
"cold-boot-invalid",
deployment.materialized(fixtures::state()).await,
)
.await,
"the cache under test is damaged from an authentic one, not invented"
);
let seeded = cache("invalid");
let authentic = seeded.path().to_path_buf();
let warm = Replica::build(&deployment, Some(seeded)).await;
demand_ok!(
recorder,
"the_seeding_replica_converges",
warm.reconciler.bootstrap().await,
"an authentic cache is exported before it is damaged"
);
drop(warm);
let bytes = demand_ok!(
recorder,
"the_exported_cache_is_readable",
std::fs::read(&authentic),
"the damaged variants are made from the file the replica wrote"
);
recorder.mark("cache-exported", format!("{} bytes", bytes.len()));
let mut edited = bytes.clone();
let last = edited.len() - 1;
edited[last] ^= 0x01;
let mut truncated = bytes.clone();
truncated.truncate(bytes.len() / 2);
let variants: [(&str, Vec<u8>, &[u8]); 3] = [
("edited-record", edited, KEY),
(
"foreign-signing-key",
bytes.clone(),
b"a-different-key-of-the-same-length--",
),
("truncated-file", truncated, KEY),
];
let mut booting = Vec::new();
for (variant, content, key) in variants {
let path = cache_path(variant);
std::fs::write(&path, &content).expect("the damaged cache is writable");
let replica = Replica::build(
&deployment,
Some(LastKnownGood::new(&path, key).expect("a long enough signing key")),
)
.await;
booting.push((variant, path, replica));
}
deployment.link.sever();
recorder.mark("severed", "the journal is unreachable for the cold boot");
recorder.observe(
"boot_note",
"the store handle is built before the cut, because `connect` refuses an unreachable \
database; what is qualified is the bootstrap decision between the cache and a refusal",
);
let mut refusals = 0u64;
for (variant, path, booting) in booting {
let generation_before = booting.generation();
let error = demand_err!(
recorder,
"the_unauthentic_cache_is_refused",
booting.reconciler.bootstrap().await,
format!("{variant}: a cache that fails its authentication is not a snapshot")
);
let cache_refused = matches!(error, BootstrapError::Cache { .. });
observe_revision_report(
&mut recorder,
&format!("{variant}_after_cold_boot"),
&booting.reconciler.report(),
);
recorder.require_that(
"the_refusal_names_the_cache",
cache_refused,
format!(
"{variant}: an operator is told the cache is the problem, not the journal: {error}"
),
);
recorder.require(
"the_snapshot_generation_does_not_move",
generation_before,
booting.generation(),
format!("{variant}: nothing unauthentic reached the served snapshot"),
);
recorder.require_that(
"no_revision_is_reported_active",
booting.reconciler.report().active.is_none(),
format!("{variant}: the replica claims no active revision after refusing"),
);
if cache_refused {
refusals += 1;
}
recorder.mark(
&format!("cold-boot-refused-{variant}"),
format!("{error} ({error:?})"),
);
recorder.observe(
&format!("refusal_{}", variant.replace('-', "_")),
error.to_string(),
);
let _ = std::fs::remove_file(&path);
}
let _ = std::fs::remove_file(&authentic);
recorder.observe("cold_start_outcome", "refused");
recorder.observe("unauthentic_cache_variants_refused", refusals);
recorder.gate(
"readiness",
spec.gate.readiness.bound(),
format!("{refusals}/3 unauthentic caches refused the boot"),
spec.gate.readiness_met(Readiness::Refuses, refusals == 3),
"an edited record, a foreign signing key, and a truncated file each refused the boot and \
published nothing",
);
recorder.deferred(
"max_serving_error_fraction",
spec.gate.max_serving_error_fraction.to_string(),
"a refusing scenario offers no traffic, so the ceiling is vacuous by contract",
);
recorder.deferred(
"max_convergence_lag_seconds",
spec.gate.max_convergence_lag_seconds.to_string(),
"a replica that never became ready is not converging",
);
recorder.deferred(
"max_data_loss_revisions",
spec.gate.max_data_loss_revisions.to_string(),
"a refused boot writes nothing; loss is measured by the restore scenarios",
);
recorder.deferred(
"admin_writes",
spec.gate.admin_writes.bound(),
"`control-plane-outage/journal-outage` measures the administrative write",
);
recorder.deferred(
"max_unauthenticated_admin_successes",
spec.gate.max_unauthenticated_admin_successes.to_string(),
"the blocked `readiness` stage owns the probe an operator's tooling calls",
);
finish(recorder);
}
#[tokio::test]
async fn recovery_convergence_journal_recovery() {
let Some(deployment) = Deployment::open().await else {
return;
};
let spec = StageSpec::load("recovery-convergence/journal-recovery");
let mut recorder = spec.recorder(&deployment);
let administrator = deployment.administrator().await;
let baseline = demand_ok!(
recorder,
"the_journal_accepts_the_baseline",
publish(
&administrator,
ExpectedRevision::Empty,
"recovery-head-baseline",
deployment.materialized(fixtures::state()).await,
)
.await,
"the fleet has a revision to converge on before the cut"
);
let survivor = Replica::build(&deployment, Some(cache("survivor"))).await;
demand_ok!(
recorder,
"the_surviving_replica_converges_before_the_outage",
survivor.reconciler.bootstrap().await,
"one replica enters the outage already serving the baseline"
);
let cold_cache = cache("cold-booter");
let cold_path = cold_cache.path().to_path_buf();
let seeding = Replica::build(&deployment, Some(cold_cache)).await;
demand_ok!(
recorder,
"the_second_replica_exports_a_cache",
seeding.reconciler.bootstrap().await,
"the other replica enters the outage with a cache to boot from"
);
drop(seeding);
recorder.mark("converged", format!("fleet at revision {}", baseline.id));
let cold_booter = Replica::build(
&deployment,
Some(LastKnownGood::new(&cold_path, KEY).expect("the same signing key")),
)
.await;
deployment.link.sever();
recorder.mark("severed", "the fleet loses the journal");
let restored_from_cache = demand_ok!(
recorder,
"the_cold_booting_replica_restores_its_cache",
cold_booter.reconciler.bootstrap().await,
"the fleet that has to converge is one survivor and one cold boot"
);
recorder.require(
"the_cold_boot_restores_the_baseline",
baseline.id,
restored_from_cache,
"both replicas enter the recovery from the same revision",
);
recorder.require_that(
"convergence_fails_while_the_journal_is_gone",
matches!(
survivor.reconciler.converge_once("qualification").await,
crate::convergence::Outcome::Rejected { .. }
),
"the step that succeeds after the recovery is the step that failed during the outage",
);
let mut head = baseline.id;
for (index, state) in [
deployment
.materialized(fixtures::state_with_renamed_alias())
.await,
deployment.materialized(fixtures::state_with_policy()).await,
]
.into_iter()
.enumerate()
{
head = demand_ok!(
recorder,
"the_journal_keeps_accepting_writes_from_elsewhere",
publish(
&direct_administrator(&deployment).await,
ExpectedRevision::Exactly(head),
&format!("recovery-during-outage-{index}"),
state,
)
.await,
"the fleet returns to a journal that moved on without it"
)
.id;
}
recorder.mark(
"published-during-outage",
format!("the journal advanced to {head} while the fleet was disconnected"),
);
recorder.observe("revisions_published_during_outage", 2u64);
demand_ok!(
recorder,
"the_link_comes_back_on_the_same_dsn",
deployment.link.restore().await,
"the recovery is the dependency returning, not the replicas being reconfigured"
);
recorder.mark("restored", "the journal is reachable again on the same DSN");
let accepted = demand_ok!(
recorder,
"the_publish_is_accepted_after_the_recovery",
publish_until_accepted(
&administrator,
ExpectedRevision::Exactly(head),
"recovery-after-restore",
deployment
.materialized(fixtures::state_with_second_tenant())
.await,
)
.await,
"administrative writes are accepted once the journal returns"
);
head = accepted.id;
recorder.mark("publish-accepted", format!("head revision {head}"));
recorder.observe("admin_write_outcome", "accepted");
let started = Instant::now();
let mut converged = Vec::new();
for (name, replica) in [("survivor", &survivor), ("cold-booter", &cold_booter)] {
let outcome = converge_until_head(replica, head).await;
let report = replica.reconciler.report();
recorder.require(
"the_replica_reaches_the_head",
head,
report
.active
.map_or_else(|| "none".to_owned(), |id| id.to_string()),
format!("{name} converged onto the revisions published while it was disconnected"),
);
recorder.require_that(
"the_replica_reports_itself_converged",
report.converged(),
format!("{name} says it is at desired state rather than only being at it"),
);
recorder.mark(
&format!("converged-{name}"),
format!("{outcome:?} after the journal returned"),
);
observe_revision_report(&mut recorder, name, &report);
recorder.observe(&format!("{name}_convergence_lag_seconds"), report.lag);
recorder.observe(
&format!("{name}_snapshot_source"),
report
.source
.map_or("unknown", crate::convergence::SnapshotSource::as_str),
);
converged.push(report.lag);
}
let recovery = started.elapsed();
let worst_lag = converged.iter().copied().max().unwrap_or_default();
recorder.observe("fleet_recovery_seconds", recovery);
recorder.observe("worst_residual_lag_seconds", worst_lag);
let trail = demand_ok!(
recorder,
"the_audit_trail_survives_the_outage",
administrator.audit_trail(head).await,
"the head published after the recovery carries its audit"
);
recorder.observe("audit_events_for_head", trail.len() as u64);
let mut surviving = 0u64;
let mut walked = Some(head);
while let Some(id) = walked {
let manifest = demand_ok!(
recorder,
"every_published_revision_is_still_readable",
administrator.load_manifest(id).await,
"the chain the fleet converged onto is walked to its root"
);
surviving += 1;
walked = manifest.parent;
}
recorder.observe("revisions_readable_after_recovery", surviving);
recorder.require(
"no_revision_is_lost_across_the_outage",
4u64,
surviving,
"the baseline, two outage-window revisions, and the post-recovery head all survive",
);
let bound = Duration::from_secs(spec.gate.max_convergence_lag_seconds);
recorder.gate(
"max_convergence_lag_seconds",
spec.gate.max_convergence_lag_seconds.to_string(),
format!("{:.3}", recovery.as_secs_f64()),
recovery <= bound,
"both replicas converged to the head revision without intervention within the bound once \
the journal returned",
);
let publish_recovered = recorder.held("the_publish_is_accepted_after_the_recovery");
recorder.gate(
"admin_writes",
spec.gate.admin_writes.bound(),
"accepted",
spec.gate
.admin_writes_met(AdminWrites::Accepted, publish_recovered),
"the publish refused during the outage succeeded against the recovered journal",
);
let nothing_was_lost = recorder.held("no_revision_is_lost_across_the_outage")
&& recorder.held("the_audit_trail_survives_the_outage");
recorder.gate(
"max_data_loss_revisions",
spec.gate.max_data_loss_revisions.to_string(),
"0",
nothing_was_lost,
"every revision the journal accepted before, during, and after the outage is readable, \
and the head's audit trail came back with it",
);
recorder.deferred(
"max_serving_error_fraction",
spec.gate.max_serving_error_fraction.to_string(),
"the blocked `serving` stage offers the requests this ceiling is measured over",
);
recorder.deferred(
"readiness",
spec.gate.readiness.bound(),
"the blocked `serving` stage owns the readiness probe",
);
recorder.deferred(
"max_unauthenticated_admin_successes",
spec.gate.max_unauthenticated_admin_successes.to_string(),
"the blocked `administration` stage authenticates administrative callers",
);
finish(recorder);
}
async fn direct_administrator(deployment: &Deployment) -> PostgresControlPlane {
let dsn = crate::test_services::postgres_dsn().expect("a configured database");
PostgresControlPlane::connect(
&dsn,
ControlPlaneSettings {
schema: Some(deployment.schema.clone()),
migrate: false,
connect_timeout: Duration::from_secs(5),
operation_timeout: Duration::from_secs(10),
..ControlPlaneSettings::default()
},
)
.await
.expect("the database itself is reachable throughout")
}
async fn publish_until_accepted(
store: &PostgresControlPlane,
expected: ExpectedRevision,
key: &str,
state: DesiredState,
) -> Result<RevisionManifest, ControlPlaneError> {
let mut last = publish(store, expected, key, state.clone()).await;
for _ in 0..50 {
match last {
Ok(manifest) => return Ok(manifest),
Err(failure) if BackendFailure::retryable(&failure) => {
tokio::time::sleep(Duration::from_millis(100)).await;
last = publish(store, expected, key, state.clone()).await;
}
Err(failure) => return Err(failure),
}
}
last
}
async fn converge_until_head(replica: &Replica, head: RevisionId) -> crate::convergence::Outcome {
let mut last = replica.reconciler.converge_once("qualification").await;
for _ in 0..50 {
if replica.reconciler.report().active == Some(head) {
return last;
}
tokio::time::sleep(Duration::from_millis(100)).await;
last = replica.reconciler.converge_once("qualification").await;
}
last
}
fn finish(recorder: Recorder) {
let artifact = recorder.finish();
let path = artifact.write();
println!("{} -> {}", artifact.summary(), path.display());
let retained = std::fs::read_to_string(&path).expect("the artifact just written is readable");
assert!(
!retained.contains(QUALIFICATION_MATERIAL),
"{}: an artifact must retain references and counts, never secret material",
path.display()
);
let failures = artifact.failures();
assert!(
failures.is_empty(),
"recovery gates failed: {failures:#?} (evidence: {})",
path.display()
);
}
#[test]
fn the_driver_runs_exactly_the_stages_the_manifest_calls_executable() {
let manifest = manifest();
let mut executable: Vec<String> = Vec::new();
for scenario in &manifest.scenarios {
for stage in &scenario.stages {
if stage.status == "executable" && stage.runner.as_deref() == Some(RUNNER) {
executable.push(format!("{}/{}", scenario.id, stage.id));
}
}
}
executable.sort();
let mut driven: Vec<String> = DRIVEN_STAGES.iter().map(|key| (*key).to_owned()).collect();
driven.sort();
assert_eq!(
executable, driven,
"the manifest and the driver disagree about which `{RUNNER}` stages run"
);
}
#[test]
fn editing_a_non_numeric_gate_changes_the_verdict() {
let text = std::fs::read_to_string(
super::evidence::workspace_root().join("qualification/recovery/manifest.toml"),
)
.expect("the recovery manifest is readable");
let declared = |manifest: &Manifest, id: &str| -> Gate {
manifest
.scenarios
.iter()
.find(|scenario| scenario.id == id)
.unwrap_or_else(|| panic!("the manifest declares `{id}`"))
.gate
};
let gate = declared(&toml_manifest(&text), "cold-boot-no-cache");
assert_eq!(gate.readiness.bound(), "refuses");
assert!(gate.readiness_met(Readiness::Refuses, true));
assert!(!gate.readiness_met(Readiness::Serves, true));
let flipped = text.replacen(
"readiness = \"refuses\"\nadmin_writes = \"unavailable\"",
"readiness = \"serves\"\nadmin_writes = \"accepted\"",
1,
);
assert_ne!(flipped, text, "the edit must reach the first refusing gate");
let gate = declared(&toml_manifest(&flipped), "cold-boot-no-cache");
assert_eq!(gate.readiness.bound(), "serves");
assert_eq!(gate.admin_writes.bound(), "accepted");
assert!(
!gate.readiness_met(Readiness::Refuses, true),
"a stage observing a refusal must fail a manifest that demands serving"
);
assert!(
!gate.admin_writes_met(AdminWrites::Unavailable, true),
"a stage observing an unavailable write must fail a manifest that demands acceptance"
);
}
#[test]
fn an_unusable_dsn_skips_locally_and_fails_where_services_are_required() {
assert!(unusable_dsn(false, "a Unix socket has no link to cut").is_none());
let required = std::panic::catch_unwind(|| unusable_dsn(true, "a Unix socket has no link"));
assert!(
required.is_err(),
"a run that promised the services must fail rather than skip"
);
}
#[test]
fn every_driven_stage_resolves_against_the_manifest() {
for key in DRIVEN_STAGES {
let spec = StageSpec::load(key);
assert_eq!(format!("{}/{}", spec.scenario, spec.stage), key);
assert!(
!spec.evidence.is_empty(),
"{key}: a driven stage retains at least one evidence class"
);
}
}