use serde::{Deserialize, Serialize};
use flint_store::{
crc64_nvme, GenerationStamps, ObjectStore, PutCondition, StoreError,
};
use super::{now_unix, LeanConfig, LeanError, LeanResult};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct InboxEntry {
pub path: String,
pub etag: String,
pub author: String,
pub added_unix: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub crc64_b64: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Window {
pub epoch: u64,
pub deadline_unix: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct VerbRequest {
pub requested_unix: u64,
pub requestor: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct InboxDoc {
pub entries: Vec<InboxEntry>,
pub window: Option<Window>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub boundary_request: Option<VerbRequest>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sync_request: Option<VerbRequest>,
}
pub struct LoadedInbox {
pub doc: InboxDoc,
pub etag: Option<String>,
}
pub async fn load(store: &dyn ObjectStore, cfg: &LeanConfig) -> LeanResult<LoadedInbox> {
match store.get_whole(&cfg.inbox_key(), None).await {
Ok((meta, bytes)) => {
let doc = serde_json::from_slice(&bytes)
.map_err(|e| LeanError::State(format!("inbox parse: {e}")))?;
Ok(LoadedInbox { doc, etag: Some(meta.etag) })
}
Err(StoreError::NotFound(_)) => Ok(LoadedInbox { doc: InboxDoc::default(), etag: None }),
Err(e) => Err(e.into()),
}
}
pub async fn cas_write(
store: &dyn ObjectStore,
cfg: &LeanConfig,
doc: &InboxDoc,
expected: Option<&str>,
epoch: u64,
) -> LeanResult<String> {
let bytes = serde_json::to_vec_pretty(doc)
.map_err(|e| LeanError::State(format!("inbox: {e}")))?;
let crc = crc64_nvme(&bytes);
let cond = match expected {
Some(etag) => PutCondition::IfMatch(etag.to_string()),
None => PutCondition::IfNoneMatchAny,
};
let stamps = GenerationStamps {
generation: 0,
epoch,
flush_uuid: "inbox".into(),
boundary_source: None,
posix: None,
};
let meta = store.put_whole(&cfg.inbox_key(), bytes.into(), &cond, &stamps, crc).await?;
Ok(meta.etag)
}
pub fn admits_hitl(doc: &InboxDoc) -> bool {
match &doc.window {
None => true,
Some(w) => now_unix() > w.deadline_unix,
}
}
pub async fn gateway_append(
store: &dyn ObjectStore,
cfg: &LeanConfig,
entry: InboxEntry,
) -> LeanResult<()> {
for _ in 0..5 {
let loaded = load(store, cfg).await?;
if !admits_hitl(&loaded.doc) {
return Err(LeanError::State(
"barrier window open — retry after the window deadline".into(),
));
}
let mut doc = loaded.doc;
doc.entries.retain(|e| e.path != entry.path);
doc.entries.push(entry.clone());
match cas_write(store, cfg, &doc, loaded.etag.as_deref(), 0).await {
Ok(_) => return Ok(()),
Err(LeanError::Store(StoreError::PreconditionFailed(_))) => continue,
Err(e) => return Err(e),
}
}
Err(LeanError::State("inbox append lost 5 CAS races".into()))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RequestedVerb {
Boundary,
Sync,
}
pub async fn gateway_request(
store: &dyn ObjectStore,
cfg: &LeanConfig,
verb: RequestedVerb,
requestor: &str,
) -> LeanResult<VerbRequest> {
let req = VerbRequest {
requested_unix: now_unix(),
requestor: requestor.chars().take(128).collect(),
};
for _ in 0..5 {
let loaded = load(store, cfg).await?;
let mut doc = loaded.doc;
match verb {
RequestedVerb::Boundary => doc.boundary_request = Some(req.clone()),
RequestedVerb::Sync => doc.sync_request = Some(req.clone()),
}
match cas_write(store, cfg, &doc, loaded.etag.as_deref(), 0).await {
Ok(_) => return Ok(req),
Err(LeanError::Store(StoreError::PreconditionFailed(_))) => continue,
Err(e) => return Err(e),
}
}
Err(LeanError::State("inbox verb request lost 5 CAS races".into()))
}
pub async fn open_window(
store: &dyn ObjectStore,
cfg: &LeanConfig,
epoch: u64,
deadline_unix: u64,
) -> LeanResult<LoadedInbox> {
for _ in 0..5 {
let loaded = load(store, cfg).await?;
if let Some(w) = &loaded.doc.window {
let expired = now_unix() > w.deadline_unix;
if w.epoch > epoch {
return Err(LeanError::Fenced(format!(
"window held by higher epoch {} (ours {})",
w.epoch, epoch
)));
}
if w.epoch == epoch && !expired {
return Ok(loaded);
}
}
let mut doc = loaded.doc.clone();
doc.window = Some(Window { epoch, deadline_unix });
match cas_write(store, cfg, &doc, loaded.etag.as_deref(), epoch).await {
Ok(etag) => {
return Ok(LoadedInbox { doc, etag: Some(etag) });
}
Err(LeanError::Store(StoreError::PreconditionFailed(_))) => continue,
Err(e) => return Err(e),
}
}
Err(LeanError::State("window open lost 5 CAS races".into()))
}
pub async fn drop_entries(
store: &dyn ObjectStore,
cfg: &LeanConfig,
epoch: u64,
consumed: &[InboxEntry],
) -> LeanResult<()> {
for _ in 0..5 {
let loaded = load(store, cfg).await?;
let mut doc = loaded.doc.clone();
let before = doc.entries.len();
doc.entries.retain(|e| !consumed.contains(e));
if doc.entries.len() == before {
return Ok(());
}
match cas_write(store, cfg, &doc, loaded.etag.as_deref(), epoch).await {
Ok(_) => return Ok(()),
Err(LeanError::Store(StoreError::PreconditionFailed(_))) => continue,
Err(e) => return Err(e),
}
}
Err(LeanError::State("inbox drop lost 5 CAS races".into()))
}
pub async fn clear_window(
store: &dyn ObjectStore,
cfg: &LeanConfig,
epoch: u64,
queued: &[InboxEntry],
) -> LeanResult<()> {
for _ in 0..5 {
let loaded = load(store, cfg).await?;
let mut doc = loaded.doc.clone();
if let Some(w) = &doc.window {
if w.epoch > epoch {
return Err(LeanError::Fenced(format!(
"window rotated to higher epoch {} (ours {})",
w.epoch, epoch
)));
}
}
doc.window = None;
for q in queued {
if !doc.entries.iter().any(|e| e.path == q.path && e.etag == q.etag) {
doc.entries.push(q.clone());
}
}
match cas_write(store, cfg, &doc, loaded.etag.as_deref(), epoch).await {
Ok(_) => return Ok(()),
Err(LeanError::Store(StoreError::PreconditionFailed(_))) => continue,
Err(e) => return Err(e),
}
}
Err(LeanError::State("window clear lost 5 CAS races".into()))
}