use anyhow::{Context, Result};
use chrono::{DateTime, Duration, Utc};
use uuid::Uuid;
use crate::memory_core::palace::Drawer;
use super::handle::PalaceHandle;
pub const TIER_C_DEFAULT_TTL_HOURS: i64 = 24;
pub const FACT_KEY_MAX_LEN: usize = 128;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TierCRefusal {
MalformedKey { key: String, detail: &'static str },
RetirementAlreadyElapsed {
key: String,
expires_at: DateTime<Utc>,
},
}
impl std::fmt::Display for TierCRefusal {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MalformedKey { key, detail } => write!(
f,
"fact_key {key:?} {detail}; expected the ADR-0028 D5 form \
<domain>:<id>/<aspect> (e.g. `pr:4818/state`). Written as an \
ordinary drawer instead"
),
Self::RetirementAlreadyElapsed { key, expires_at } => write!(
f,
"fact_key {key:?} was given expires_at {expires_at} which has \
already elapsed, so the fact declares no live window; it \
cannot claim a slot or retire that slot's occupant. Written \
as an ordinary drawer instead"
),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TierCAdmission {
NotRequested,
Admitted {
fact_key: String,
expires_at: DateTime<Utc>,
},
Refused(TierCRefusal),
}
impl TierCAdmission {
pub fn tier_label(&self) -> &'static str {
match self {
Self::Admitted { .. } => "C",
_ => "E",
}
}
}
pub fn validate_fact_key(key: &str) -> Result<(), &'static str> {
if key.is_empty() {
return Err("is empty");
}
if key.len() > FACT_KEY_MAX_LEN {
return Err("is longer than 128 bytes");
}
let Some((domain, rest)) = key.split_once(':') else {
return Err("has no `<domain>:` prefix");
};
let Some((id, aspect)) = rest.split_once('/') else {
return Err("has no `/<aspect>` suffix");
};
for segment in [domain, id, aspect] {
if segment.is_empty() {
return Err("has an empty <domain>, <id>, or <aspect> segment");
}
if !segment
.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'-' | b'_'))
{
return Err("has a segment outside [A-Za-z0-9._-]");
}
}
Ok(())
}
pub fn admit_tier_c(
fact_key: Option<&str>,
expires_at: Option<DateTime<Utc>>,
now: DateTime<Utc>,
) -> TierCAdmission {
let Some(key) = fact_key else {
return TierCAdmission::NotRequested;
};
if let Err(detail) = validate_fact_key(key) {
return TierCAdmission::Refused(TierCRefusal::MalformedKey {
key: key.to_string(),
detail,
});
}
let resolved = match expires_at {
Some(t) if t > now => t,
Some(t) => {
return TierCAdmission::Refused(TierCRefusal::RetirementAlreadyElapsed {
key: key.to_string(),
expires_at: t,
});
}
None => now + Duration::hours(TIER_C_DEFAULT_TTL_HOURS),
};
TierCAdmission::Admitted {
fact_key: key.to_string(),
expires_at: resolved,
}
}
pub(super) fn apply_admission(
drawer: &mut Drawer,
opts: &super::types::RememberOptions,
palace: &crate::memory_core::palace::PalaceId,
) {
match admit_tier_c(opts.fact_key.as_deref(), opts.expires_at, Utc::now()) {
TierCAdmission::Admitted {
fact_key,
expires_at,
} => {
drawer.fact_key = Some(fact_key);
drawer.expires_at = Some(expires_at);
}
TierCAdmission::Refused(refusal) => {
tracing::warn!(palace = %palace, "#4886 tier C admission refused: {refusal}");
}
TierCAdmission::NotRequested => {
if let Some(t) = opts.expires_at {
drawer.expires_at = Some(t);
}
}
}
}
pub(super) fn retire_in_memory(drawers: &mut [Drawer], retired_id: Option<Uuid>) {
let Some(retired_id) = retired_id else {
return;
};
for d in drawers.iter_mut().filter(|d| d.id == retired_id) {
d.fact_key = None;
d.expires_at = None;
}
}
pub(super) async fn persist_with_retirement(
handle: &PalaceHandle,
drawer: &Drawer,
) -> Result<Option<Uuid>> {
let Some(key) = drawer.fact_key.as_deref() else {
handle.kg.upsert_drawer(drawer).await?;
return Ok(None);
};
let incumbent_id = handle
.kg
.drawer_id_for_fact_key(key)
.context("resolve current fact_key slot occupant")?
.filter(|id| *id != drawer.id);
let Some(incumbent_id) = incumbent_id else {
handle.kg.upsert_drawer(drawer).await?;
return Ok(None);
};
let incumbent = handle
.drawers
.read()
.iter()
.find(|d| d.id == incumbent_id)
.cloned();
let Some(incumbent) = incumbent else {
tracing::warn!(
palace = %handle.id,
fact_key = %key,
incumbent = %incumbent_id,
"#4886: fact_key slot names a drawer absent from the in-memory \
table; writing the newcomer only — the index still moves, so the \
slot keeps exactly one indexed claimant"
);
handle.kg.upsert_drawer(drawer).await?;
return Ok(None);
};
let mut retired = incumbent;
retired.fact_key = None;
retired.expires_at = None;
handle
.kg
.upsert_drawers_atomic(vec![retired, drawer.clone()])
.await
.context("commit tier-c retirement and replacement")?;
Ok(Some(incumbent_id))
}
#[cfg(test)]
mod tests {
use super::*;
fn t(offset_hours: i64) -> DateTime<Utc> {
DateTime::from_timestamp(1_700_000_000, 0).expect("fixed epoch")
+ Duration::hours(offset_hours)
}
#[test]
fn accepts_the_adr_example_keys() {
for key in [
"pr:4818/state",
"ws:tm-trusty-tools-03/resume",
"daemon:trusty-search/install-state",
] {
assert!(validate_fact_key(key).is_ok(), "{key} should be valid");
}
}
#[test]
fn refuses_a_bare_unnamespaced_key() {
assert_eq!(validate_fact_key("state"), Err("has no `<domain>:` prefix"));
assert_eq!(
validate_fact_key("pr:4818"),
Err("has no `/<aspect>` suffix")
);
}
#[test]
fn refuses_keys_with_empty_segments() {
for key in [":4818/state", "pr:/state", "pr:4818/"] {
assert!(validate_fact_key(key).is_err(), "{key} should be refused");
}
}
#[test]
fn refuses_an_over_long_key() {
let key = format!("pr:{}/state", "x".repeat(FACT_KEY_MAX_LEN));
assert_eq!(validate_fact_key(&key), Err("is longer than 128 bytes"));
}
#[test]
fn refuses_a_key_with_extra_separators() {
assert!(validate_fact_key("pr:4818/state/extra").is_err());
assert!(validate_fact_key("pr:48:18/state").is_err());
}
#[test]
fn no_fact_key_is_not_a_tier_c_request() {
assert_eq!(admit_tier_c(None, None, t(0)), TierCAdmission::NotRequested);
assert_eq!(
admit_tier_c(None, Some(t(1)), t(0)),
TierCAdmission::NotRequested
);
}
#[test]
fn admits_a_well_formed_key_with_the_default_ttl() {
let now = t(0);
assert_eq!(
admit_tier_c(Some("pr:4818/state"), None, now),
TierCAdmission::Admitted {
fact_key: "pr:4818/state".to_string(),
expires_at: now + Duration::hours(TIER_C_DEFAULT_TTL_HOURS),
}
);
}
#[test]
fn admits_an_explicit_future_expiry_unchanged() {
let now = t(0);
let ttl = t(3);
assert_eq!(
admit_tier_c(Some("pr:4818/state"), Some(ttl), now),
TierCAdmission::Admitted {
fact_key: "pr:4818/state".to_string(),
expires_at: ttl,
}
);
}
#[test]
fn refuses_an_already_elapsed_ttl() {
let now = t(0);
let admission = admit_tier_c(Some("pr:4818/state"), Some(t(-1)), now);
assert!(matches!(
admission,
TierCAdmission::Refused(TierCRefusal::RetirementAlreadyElapsed { .. })
));
assert_eq!(admission.tier_label(), "E");
assert!(matches!(
admit_tier_c(Some("pr:4818/state"), Some(now), now),
TierCAdmission::Refused(TierCRefusal::RetirementAlreadyElapsed { .. })
));
}
#[test]
fn a_malformed_key_is_refused_not_admitted_with_a_default() {
let admission = admit_tier_c(Some("state"), None, t(0));
assert!(matches!(
admission,
TierCAdmission::Refused(TierCRefusal::MalformedKey { .. })
));
assert_eq!(admission.tier_label(), "E");
}
#[test]
fn refusal_display_names_the_key_and_the_degradation() {
let msg = TierCRefusal::MalformedKey {
key: "state".into(),
detail: "has no `<domain>:` prefix",
}
.to_string();
assert!(msg.contains("state"), "{msg}");
assert!(msg.contains("ordinary drawer"), "{msg}");
}
}