1use serde::{Deserialize, Serialize};
11
12use flint_store::{
13 crc64_nvme, GenerationStamps, ObjectStore, PutCondition, StoreError,
14};
15
16use super::{now_unix, LeanConfig, LeanError, LeanResult};
17
18#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
23pub struct VerbRequest {
24 pub requested_unix: u64,
25 pub requestor: String,
26}
27
28#[derive(Debug, Clone, Default, Serialize, Deserialize)]
29pub struct InboxDoc {
30 #[serde(default, skip_serializing_if = "Option::is_none")]
32 pub boundary_request: Option<VerbRequest>,
33 #[serde(default, skip_serializing_if = "Option::is_none")]
42 pub sync_request: Option<VerbRequest>,
43}
44
45pub struct LoadedInbox {
46 pub doc: InboxDoc,
47 pub etag: Option<String>,
49}
50
51pub async fn load(store: &dyn ObjectStore, cfg: &LeanConfig) -> LeanResult<LoadedInbox> {
52 match store.get_whole(&cfg.inbox_key(), None).await {
53 Ok((meta, bytes)) => {
54 let doc = serde_json::from_slice(&bytes)
55 .map_err(|e| LeanError::State(format!("inbox parse: {e}")))?;
56 Ok(LoadedInbox { doc, etag: Some(meta.etag) })
57 }
58 Err(StoreError::NotFound(_)) => Ok(LoadedInbox { doc: InboxDoc::default(), etag: None }),
59 Err(e) => Err(e.into()),
60 }
61}
62
63fn lost_race(e: &LeanError) -> bool {
72 matches!(e, LeanError::Store(StoreError::PreconditionFailed(_) | StoreError::Conflict(_)))
73}
74
75pub async fn cas_write(
76 store: &dyn ObjectStore,
77 cfg: &LeanConfig,
78 doc: &InboxDoc,
79 expected: Option<&str>,
80 epoch: u64,
81) -> LeanResult<String> {
82 let bytes = serde_json::to_vec_pretty(doc)
83 .map_err(|e| LeanError::State(format!("inbox: {e}")))?;
84 let crc = crc64_nvme(&bytes);
85 let cond = match expected {
86 Some(etag) => PutCondition::IfMatch(etag.to_string()),
87 None => PutCondition::IfNoneMatchAny,
88 };
89 let stamps = GenerationStamps {
90 generation: 0,
91 epoch,
92 flush_uuid: "inbox".into(),
93 boundary_source: None,
94 posix: None,
95 };
96 let meta = store.put_whole(&cfg.inbox_key(), bytes.into(), &cond, &stamps, crc).await?;
97 Ok(meta.etag)
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub enum RequestedVerb {
103 Boundary,
104 Sync,
105}
106
107pub async fn gateway_request(
110 store: &dyn ObjectStore,
111 cfg: &LeanConfig,
112 verb: RequestedVerb,
113 requestor: &str,
114) -> LeanResult<VerbRequest> {
115 let req = VerbRequest {
116 requested_unix: now_unix(),
117 requestor: requestor.chars().take(128).collect(),
118 };
119 for _ in 0..5 {
120 let loaded = load(store, cfg).await?;
121 let mut doc = loaded.doc;
122 match verb {
127 RequestedVerb::Boundary => doc.boundary_request = Some(req.clone()),
128 RequestedVerb::Sync => doc.sync_request = Some(req.clone()),
129 }
130 match cas_write(store, cfg, &doc, loaded.etag.as_deref(), 0).await {
131 Ok(_) => return Ok(req),
132 Err(e) if lost_race(&e) => continue,
133 Err(e) => return Err(e),
134 }
135 }
136 Err(LeanError::State("inbox verb request lost 5 CAS races".into()))
137}