use crate::state::StoreConformance;
use crate::{now_unix, Access, LeanResult, Syncer, LEAN_DIR};
pub const PROBE_VERSION: u32 = 1;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Decision {
Conformant,
CollectorOff(String),
Refuse(String),
Unknown(String),
}
pub async fn gate(sc: &mut Syncer, identity: &str) -> LeanResult<Decision> {
if sc.cfg.access == Access::Read {
return Ok(Decision::Conformant);
}
let cached = sc.state.load_conformance()?.filter(|c| {
c.identity == identity && c.probe_version == PROBE_VERSION
});
let verdict = match cached {
Some(c) => c,
None => {
let c = probe(sc, identity).await;
if c.answered {
sc.state.save_conformance(&c)?;
}
c
}
};
sc.trace(
"conformance",
serde_json::json!({
"identity": verdict.identity, "answered": verdict.answered,
"put": verdict.conditional_put, "delete": verdict.conditional_delete,
"detail": verdict.detail,
}),
);
if !verdict.answered {
return Ok(Decision::Unknown(verdict.detail.unwrap_or_default()));
}
if !verdict.conditional_put {
return Ok(Decision::Refuse(verdict.detail.unwrap_or_default()));
}
if !verdict.conditional_delete {
return Ok(Decision::CollectorOff(verdict.detail.unwrap_or_default()));
}
Ok(Decision::Conformant)
}
async fn probe(sc: &Syncer, identity: &str) -> StoreConformance {
let who = std::env::var("HOSTNAME")
.ok()
.filter(|h| !h.is_empty())
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
let mut out = StoreConformance {
identity: identity.to_string(),
probe_version: PROBE_VERSION,
answered: true,
conditional_put: false,
conditional_delete: false,
detail: None,
at_unix: now_unix(),
};
let unanswered = |e: &str| e.starts_with(flint_store::probe::PROBE_UNREACHABLE);
let put_key = format!("{}/{}/probe/conditional-put-{who}", sc.cfg.prefix, LEAN_DIR);
if let Err(e) = flint_store::probe::probe_conditional_writes(sc.store.as_ref(), &put_key).await
{
out.answered = !unanswered(&e);
out.detail = Some(e);
return out;
}
out.conditional_put = true;
let del_key = format!("{}/{}/probe/conditional-delete-{who}", sc.cfg.prefix, LEAN_DIR);
match flint_store::probe::probe_conditional_delete(sc.store.as_ref(), &del_key).await {
Ok(()) => out.conditional_delete = true,
Err(e) if unanswered(&e) => {
out.answered = false;
out.detail = Some(e);
}
Err(e) => out.detail = Some(e),
}
out
}
pub fn message(d: &Decision, prefix: &str) -> String {
match d {
Decision::Conformant => "conditional writes enforced (PUT and DELETE)".into(),
Decision::CollectorOff(why) => format!(
"this store does not enforce If-Match on DELETE ({why}). Under immutable object \
handles that costs nothing: every write lands at a handle nobody else writes, \
and a handle the installed document stopped citing can never be cited again, so \
the collector's DELETE has nothing left to guard and runs unconditionally here \
as everywhere. The syncer publishes and collects normally under {prefix}/files/. \
Reported because it is a real property of this store — it was the posture switch \
before handles, and the one the conditional-DELETE row of lean/SAFETY.md §2 was \
written for"
),
Decision::Refuse(why) => format!(
"this store does not enforce conditional writes ({why}). Lean arbitrates \
ENTIRELY through them — the manifest CAS, the publish fence and every upload \
— so on this store two writers would silently overwrite each other instead of \
conflicting. Refusing rather than degrading; see lean/SAFETY.md §2"
),
Decision::Unknown(why) => format!(
"could not ask this store whether it enforces conditional writes ({why}) — the \
probe could not write its own object. Continuing UNVERIFIED, exactly as before \
this check existed; the verb below will report the underlying error itself"
),
}
}