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 VerbRequest {
pub requested_unix: u64,
pub requestor: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct InboxDoc {
#[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()),
}
}
fn lost_race(e: &LeanError) -> bool {
matches!(e, LeanError::Store(StoreError::PreconditionFailed(_) | StoreError::Conflict(_)))
}
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)
}
#[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(e) if lost_race(&e) => continue,
Err(e) => return Err(e),
}
}
Err(LeanError::State("inbox verb request lost 5 CAS races".into()))
}