1use super::*;
21use sha2::{Digest, Sha256};
22use std::collections::BTreeMap;
23use std::sync::atomic::{AtomicBool, AtomicU64, AtomicU8, Ordering};
24use std::sync::Mutex;
25
26fn now_unix() -> u64 {
27 std::time::SystemTime::now()
28 .duration_since(std::time::UNIX_EPOCH)
29 .map(|d| d.as_secs())
30 .unwrap_or(0)
31}
32
33#[derive(Clone)]
34struct StoredObject {
35 bytes: Bytes,
36 etag: String,
37 crc64: u64,
38 meta: HashMap<String, String>,
39 last_modified_unix: u64,
40 version_id: String,
46 deleted: bool,
50}
51
52impl StoredObject {
53 fn to_meta(&self) -> ObjectMeta {
54 ObjectMeta {
55 etag: self.etag.clone(),
56 size: self.bytes.len() as u64,
57 crc64_b64: Some(crc64_to_b64(self.crc64)),
58 meta: self.meta.clone(),
59 last_modified_unix: Some(self.last_modified_unix),
60 storage_class: None,
63 version_id: Some(self.version_id.clone()),
64 }
65 }
66}
67
68struct Mpu {
69 key: String,
70 parts: BTreeMap<usize, Bytes>,
71 initiated_unix: u64,
72}
73
74#[derive(Default, Clone)]
79struct VersionChain {
80 versions: Vec<StoredObject>,
81}
82
83impl VersionChain {
84 fn current(&self) -> Option<&StoredObject> {
87 match self.versions.last() {
88 Some(o) if !o.deleted => Some(o),
89 _ => None,
90 }
91 }
92}
93
94#[derive(Default)]
95struct Inner {
96 chains: BTreeMap<String, VersionChain>,
97 uploads: HashMap<String, Mpu>,
98 version_seq: u64,
100 lifecycle: Vec<LifecycleView>,
104}
105
106impl Inner {
107 fn next_version_id(&mut self) -> String {
108 self.version_seq += 1;
109 format!("v{:012}", self.version_seq)
110 }
111
112 fn current(&self, key: &str) -> Option<&StoredObject> {
114 self.chains.get(key).and_then(|c| c.current())
115 }
116
117 fn push(&mut self, key: &str, mut obj: StoredObject) -> String {
119 let vid = self.next_version_id();
120 obj.version_id = vid.clone();
121 self.chains.entry(key.to_string()).or_default().versions.push(obj);
122 vid
123 }
124
125 fn push_delete_marker(&mut self, key: &str) {
127 if self.chains.get(key).map(|c| c.current().is_none()).unwrap_or(true) {
128 return; }
130 let vid = self.next_version_id();
131 self.chains.entry(key.to_string()).or_default().versions.push(StoredObject {
132 bytes: Bytes::new(),
133 etag: String::new(),
134 crc64: 0,
135 meta: HashMap::new(),
136 last_modified_unix: now_unix(),
137 version_id: vid,
138 deleted: true,
139 });
140 }
141
142 fn version(&self, key: &str, version_id: &str) -> Option<&StoredObject> {
143 self.chains
144 .get(key)?
145 .versions
146 .iter()
147 .find(|o| o.version_id == version_id && !o.deleted)
148 }
149
150 fn remove_version(&mut self, key: &str, version_id: &str) {
154 if let Some(chain) = self.chains.get_mut(key) {
155 chain.versions.retain(|o| o.version_id != version_id);
156 if chain.versions.is_empty() {
157 self.chains.remove(key);
158 }
159 }
160 }
161}
162
163const INJECT_NONE: u8 = 0;
164const INJECT_TORN_COMPLETE: u8 = 1;
165const INJECT_CRASH_BEFORE_COMPLETE: u8 = 2;
166const INJECT_SWEPT: u8 = 3;
167
168pub struct MemoryStore {
169 inner: Mutex<Inner>,
170 upload_seq: AtomicU64,
171 inject: AtomicU8,
173 leave_orphan: AtomicBool,
176 fail_get_range_count: AtomicU64,
178 fail_head_count: AtomicU64,
180 fail_epoch_renew_count: AtomicU64,
183 fail_put_after_land_count: AtomicU64,
186 fail_put_after_land_key: Mutex<String>,
187 stall_next_get_range_ms: AtomicU64,
188 get_range_delay_ms: AtomicU64,
192 put_whole_delay_ms: AtomicU64,
196 hold_puts: AtomicBool,
201 hold_puts_matching: Mutex<Option<String>>,
204 held_puts: Mutex<Vec<tokio::sync::oneshot::Sender<()>>>,
205 inflight_get_range: AtomicU64,
208 peak_get_range: AtomicU64,
209 strip_version_ids: AtomicBool,
214 fail_lifecycle_writes: AtomicBool,
218 if_match_missing_412: AtomicBool,
225 ops: Mutex<std::collections::BTreeMap<&'static str, u64>>,
229 pub min_part: u64,
230 pub max_parts: usize,
231 part_parallelism: usize,
235 upload_gate: Option<std::sync::Arc<crate::gate::ByteGate>>,
239}
240
241struct InFlight<'a>(&'a AtomicU64);
244impl Drop for InFlight<'_> {
245 fn drop(&mut self) {
246 self.0.fetch_sub(1, Ordering::SeqCst);
247 }
248}
249
250impl Default for MemoryStore {
251 fn default() -> Self {
252 Self::new()
253 }
254}
255
256impl MemoryStore {
257 pub fn backdate_epoch(&self, key: &str, secs: u64) {
261 let mut inner = self.inner.lock().unwrap();
262 if let Some(chain) = inner.chains.get_mut(key) {
263 for v in chain.versions.iter_mut() {
264 v.last_modified_unix = v.last_modified_unix.saturating_sub(secs);
265 }
266 }
267 }
268
269 pub fn plant_lifecycle_rule(&self, rule: LifecycleView) {
272 self.inner.lock().unwrap().lifecycle.push(rule);
273 }
274
275 pub fn new() -> Self {
276 MemoryStore {
277 ops: Mutex::new(Default::default()),
278 inner: Mutex::new(Inner::default()),
279 upload_seq: AtomicU64::new(1),
280 inject: AtomicU8::new(INJECT_NONE),
281 leave_orphan: AtomicBool::new(false),
282 fail_get_range_count: AtomicU64::new(0),
283 fail_head_count: AtomicU64::new(0),
284 fail_epoch_renew_count: AtomicU64::new(0),
285 fail_put_after_land_count: AtomicU64::new(0),
286 fail_put_after_land_key: Mutex::new(String::new()),
287 stall_next_get_range_ms: AtomicU64::new(0),
288 get_range_delay_ms: AtomicU64::new(0),
289 put_whole_delay_ms: AtomicU64::new(0),
290 hold_puts: AtomicBool::new(false),
291 hold_puts_matching: Mutex::new(None),
292 held_puts: Mutex::new(Vec::new()),
293 inflight_get_range: AtomicU64::new(0),
294 peak_get_range: AtomicU64::new(0),
295 strip_version_ids: AtomicBool::new(false),
296 fail_lifecycle_writes: AtomicBool::new(false),
297 if_match_missing_412: AtomicBool::new(false),
298 min_part: 1,
301 max_parts: 10_000,
302 part_parallelism: 1,
303 upload_gate: None,
304 }
305 }
306
307 pub fn with_if_match_missing_as_412(self) -> Self {
310 self.if_match_missing_412.store(true, Ordering::SeqCst);
311 self
312 }
313
314 pub fn with_part_parallelism(mut self, n: usize) -> Self {
317 self.part_parallelism = n.max(1);
318 self
319 }
320
321 pub fn with_upload_inflight_max_bytes(mut self, n: u64) -> Self {
324 self.upload_gate = (n > 0).then(|| std::sync::Arc::new(crate::gate::ByteGate::new(n)));
325 self
326 }
327
328 pub fn inject_put_whole_delay_ms(&self, ms: u64) {
334 self.put_whole_delay_ms.store(ms, Ordering::SeqCst);
335 }
336
337 pub fn strip_version_ids(&self, on: bool) {
340 self.strip_version_ids.store(on, Ordering::SeqCst);
341 }
342
343 pub fn fail_lifecycle_writes(&self, on: bool) {
345 self.fail_lifecycle_writes.store(on, Ordering::SeqCst);
346 }
347
348 pub fn inject_torn_complete(&self) {
350 self.inject.store(INJECT_TORN_COMPLETE, Ordering::SeqCst);
351 }
352
353 pub fn inject_crash_before_complete(&self) {
355 self.inject.store(INJECT_CRASH_BEFORE_COMPLETE, Ordering::SeqCst);
356 }
357
358 pub fn inject_compose_swept(&self) {
361 self.inject.store(INJECT_SWEPT, Ordering::SeqCst);
362 }
363
364 pub fn inject_get_range_failure(&self) {
367 self.inject_get_range_failures(1);
368 }
369
370 pub fn inject_get_range_failures(&self, n: u64) {
373 self.fail_get_range_count.store(n, Ordering::SeqCst);
374 }
375
376 pub fn inject_get_range_delay_ms(&self, ms: u64) {
382 self.get_range_delay_ms.store(ms, Ordering::SeqCst);
383 }
384
385 pub fn inject_put_hold(&self, on: bool) {
391 *self.hold_puts_matching.lock().unwrap() = None;
392 self.hold_puts.store(on, Ordering::SeqCst);
393 }
394
395 pub fn inject_put_hold_matching(&self, key_contains: &str) {
400 *self.hold_puts_matching.lock().unwrap() = Some(key_contains.to_string());
401 self.hold_puts.store(true, Ordering::SeqCst);
402 }
403
404 pub fn held_puts(&self) -> usize {
406 self.held_puts.lock().unwrap().len()
407 }
408
409 pub fn release_held_puts(&self) {
411 for tx in self.held_puts.lock().unwrap().drain(..) {
412 let _ = tx.send(());
413 }
414 }
415
416 pub fn peak_get_range_in_flight(&self) -> u64 {
420 self.peak_get_range.load(Ordering::SeqCst)
421 }
422 pub fn reset_peak_get_range_in_flight(&self) {
423 self.peak_get_range.store(0, Ordering::SeqCst);
424 }
425
426 pub fn inject_head_failures(&self, n: u64) {
433 self.fail_head_count.store(n, Ordering::SeqCst);
434 }
435
436 pub fn inject_epoch_renew_failures(&self, n: u64) {
439 self.fail_epoch_renew_count.store(n, Ordering::SeqCst);
440 }
441
442 pub fn inject_put_lands_then_fails(&self, key_contains: &str, n: u64) {
455 *self.fail_put_after_land_key.lock().unwrap() = key_contains.to_string();
456 self.fail_put_after_land_count.store(n, Ordering::SeqCst);
457 }
458
459 pub fn inject_get_range_stall(&self, ms: u64) {
462 self.stall_next_get_range_ms.store(ms, Ordering::SeqCst);
463 }
464
465 pub fn inject_corrupt_body(&self, key: &str, mutate: impl FnOnce(&mut Vec<u8>)) {
472 let mut inner = self.inner.lock().unwrap();
473 let chain = inner.chains.get_mut(key).expect("corrupting a key that exists");
474 let obj = chain.versions.last_mut().expect("a version to corrupt");
475 let mut b = obj.bytes.to_vec();
476 mutate(&mut b);
477 obj.bytes = Bytes::from(b);
478 }
479
480 pub fn raw_put(&self, key: &str, bytes: Bytes, meta: Vec<(String, String)>) -> ObjectMeta {
483 let obj = StoredObject {
484 etag: put_etag(&bytes),
485 crc64: crc64_nvme(&bytes),
486 meta: meta.into_iter().collect(),
487 last_modified_unix: now_unix(),
488 bytes,
489 version_id: String::new(),
490 deleted: false,
491 };
492 let mut inner = self.inner.lock().unwrap();
493 let vid = inner.push(key, obj);
494 let mut m = inner.current(key).map(|o| o.to_meta()).expect("just pushed");
495 m.version_id = Some(vid);
496 m
497 }
498
499 pub fn redeem_presigned_put(
517 &self,
518 url: &str,
519 headers: &BTreeMap<String, String>,
520 body: Bytes,
521 ) -> StoreResult<ObjectMeta> {
522 let rest = url
523 .strip_prefix("memory://presigned-put/")
524 .ok_or_else(|| StoreError::Other(format!("not a memory presigned PUT: {url}")))?;
525 let (key, query) = rest.split_once('?').unwrap_or((rest, ""));
526 let presented: BTreeMap<String, &String> =
527 headers.iter().map(|(k, v)| (k.to_ascii_lowercase(), v)).collect();
528 for param in query.split('&') {
529 if let Some((name, want)) = param.split_once('=').and_then(|(k, v)| Some((k.strip_prefix("h.")?, v))) {
530 if presented.get(name).map(|v| v.as_str()) != Some(want) {
531 return Err(StoreError::Auth(format!(
532 "SignatureDoesNotMatch: signed header {name} not presented as signed"
533 )));
534 }
535 }
536 }
537 if let Some(sum) = presented.get("x-amz-checksum-sha256") {
538 let actual = crate::base64_std(&Sha256::digest(&body));
539 if **sum != actual {
540 return Err(StoreError::ChecksumMismatch(format!(
541 "BadDigest: {key}: x-amz-checksum-sha256 {sum} but the body hashes to {actual}"
542 )));
543 }
544 }
545 if presented.get("if-none-match").map(|v| v.as_str()) == Some("*")
546 && self.inner.lock().unwrap().current(key).is_some()
547 {
548 return Err(StoreError::PreconditionFailed(format!("{key} exists")));
549 }
550 Ok(self.raw_put(key, body, Vec::new()))
551 }
552
553 pub fn expire_noncurrent(&self, older_than_secs: u64) -> Vec<(String, String)> {
569 let now = now_unix();
570 let mut inner = self.inner.lock().unwrap();
571 let mut reaped = vec![];
572 let keys: Vec<String> = inner.chains.keys().cloned().collect();
573 for k in keys {
574 let Some(chain) = inner.chains.get_mut(&k) else { continue };
575 let last = chain.versions.len().saturating_sub(1);
576 let doomed: Vec<String> = chain
577 .versions
578 .iter()
579 .enumerate()
580 .filter(|(i, o)| {
581 *i != last && now.saturating_sub(o.last_modified_unix) >= older_than_secs
582 })
583 .map(|(_, o)| o.version_id.clone())
584 .collect();
585 for vid in doomed {
586 chain.versions.retain(|o| o.version_id != vid);
587 reaped.push((k.clone(), vid));
588 }
589 }
590 reaped
591 }
592
593 pub fn version_count(&self, key: &str) -> usize {
598 self.inner.lock().unwrap().chains.get(key).map(|c| c.versions.len()).unwrap_or(0)
599 }
600
601 pub fn raw_complete_upload(&self, upload_id: &str) -> StoreResult<ObjectMeta> {
607 let mut inner = self.inner.lock().unwrap();
608 let Some(mpu) = inner.uploads.remove(upload_id) else {
609 return Err(StoreError::NoSuchUpload(format!("upload {}", upload_id)));
610 };
611 let mut bytes = Vec::new();
612 let n_parts = mpu.parts.len().max(1);
613 for part in mpu.parts.values() {
614 bytes.extend_from_slice(part);
615 }
616 let bytes = Bytes::from(bytes);
617 let obj = StoredObject {
618 etag: mpu_etag(&bytes, n_parts),
619 crc64: crc64_nvme(&bytes),
620 meta: HashMap::new(),
621 last_modified_unix: now_unix(),
622 bytes,
623 version_id: String::new(),
624 deleted: false,
625 };
626 let mkey = mpu.key.clone();
627 let vid = inner.push(&mkey, obj);
628 let mut m = inner.current(&mkey).expect("just pushed").to_meta();
629 m.version_id = Some(vid);
630 Ok(m)
631 }
632
633 pub fn raw_begin_upload(&self, key: &str) -> String {
635 let id = format!("mpu-{}", self.upload_seq.fetch_add(1, Ordering::SeqCst));
636 self.inner.lock().unwrap().uploads.insert(
637 id.clone(),
638 Mpu { key: key.to_string(), parts: BTreeMap::new(), initiated_unix: now_unix() },
639 );
640 id
641 }
642
643 fn check_condition(
644 &self,
645 existing: Option<&StoredObject>,
646 condition: &PutCondition,
647 ) -> StoreResult<()> {
648 match (condition, existing) {
649 (PutCondition::IfMatch(want), Some(o)) if o.etag == *want => Ok(()),
650 (PutCondition::IfMatch(_), Some(_)) => {
651 Err(StoreError::PreconditionFailed("If-Match: etag differs".into()))
652 }
653 (PutCondition::IfMatch(_), None) if self.if_match_missing_412.load(Ordering::SeqCst) => {
654 Err(StoreError::PreconditionFailed("If-Match: no object".into()))
655 }
656 (PutCondition::IfMatch(_), None) => Err(StoreError::NotFound("NoSuchKey: If-Match on a missing key".into())),
657 (PutCondition::IfNoneMatchAny, None) => Ok(()),
658 (PutCondition::IfNoneMatchAny, Some(_)) => {
659 Err(StoreError::PreconditionFailed("If-None-Match: object exists".into()))
660 }
661 (PutCondition::Unconditional, _) => Ok(()),
662 }
663 }
664}
665
666fn put_etag(bytes: &[u8]) -> String {
667 let mut h = Sha256::new();
668 h.update(bytes);
669 format!("\"{:x}\"", h.finalize())
670}
671
672fn mpu_etag(bytes: &[u8], parts: usize) -> String {
676 let mut h = Sha256::new();
677 h.update(bytes);
678 format!("\"{:x}-{}\"", h.finalize(), parts)
679}
680
681#[derive(serde::Serialize, serde::Deserialize)]
682struct EpochBody {
683 holder_id: String,
684 epoch: u64,
685 #[serde(default)]
687 released: bool,
688 #[serde(default, skip_serializing_if = "Option::is_none")]
690 echo: Option<String>,
691 #[serde(default, skip_serializing_if = "Vec::is_empty")]
693 waiters: Vec<String>,
694 #[serde(default, skip_serializing_if = "Option::is_none")]
695 handoff: Option<String>,
696}
697
698impl MemoryStore {
699 fn epoch_store(&self, key: &str, body: EpochBody, condition: &PutCondition) -> StoreResult<String> {
704 let bytes = Bytes::from(serde_json::to_vec(&body).unwrap());
705 let mut inner = self.inner.lock().unwrap();
706 self.check_condition(inner.current(key), condition)?;
707 let mut salted = bytes.to_vec();
708 salted.extend_from_slice(&self.upload_seq.fetch_add(1, Ordering::SeqCst).to_be_bytes());
709 let obj = StoredObject {
710 etag: put_etag(&salted),
711 crc64: crc64_nvme(&bytes),
712 meta: HashMap::new(),
713 last_modified_unix: now_unix(),
714 bytes,
715 version_id: String::new(),
716 deleted: false,
717 };
718 let token = obj.etag.clone();
719 inner.push(key, obj);
720 Ok(token)
721 }
722
723 fn bump(&self, op: &'static str) {
724 *self.ops.lock().unwrap().entry(op).or_insert(0) += 1;
725 }
726 pub fn op_counts(&self) -> std::collections::BTreeMap<&'static str, u64> {
728 self.ops.lock().unwrap().clone()
729 }
730 pub fn reset_op_counts(&self) {
731 self.ops.lock().unwrap().clear();
732 }
733 pub fn total_ops(&self) -> u64 {
734 self.ops.lock().unwrap().values().sum()
735 }
736}
737
738#[async_trait]
739impl ObjectStore for MemoryStore {
740 fn request_counts(&self) -> Option<crate::RequestCounts> {
741 let mut c = crate::RequestCounts::default();
744 for (op, n) in self.op_counts() {
745 match op {
746 "get_whole" | "get_range" | "get_version" | "epoch_read" | "presign_get" => c.get += n,
747 "head" | "head_version" => c.head += n,
748 "put_whole" | "epoch_acquire" | "epoch_renew" | "epoch_release" | "epoch_handoff"
749 | "epoch_enqueue" | "presign_put" => c.put += n,
750 "copy_object" => c.copy += n,
751 "delete" | "delete_version" => c.delete += n,
752 "list" | "list_versions" | "list_uploads" => c.list += n,
753 "compose_generation" | "compose_inner" | "abort_upload" => c.multipart += n,
754 _ => {}
755 }
756 }
757 Some(c)
758 }
759
760 async fn put_whole(
761 &self,
762 key: &str,
763 body: Bytes,
764 condition: &PutCondition,
765 stamps: &GenerationStamps,
766 crc64: u64,
767 ) -> StoreResult<ObjectMeta> {
768 self.bump("put_whole");
769 let held = self.hold_puts.load(Ordering::SeqCst)
770 && match self.hold_puts_matching.lock().unwrap().as_deref() {
771 None => true,
772 Some(m) => key.contains(m),
773 };
774 if held {
775 let (tx, rx) = tokio::sync::oneshot::channel();
776 self.held_puts.lock().unwrap().push(tx);
777 let _ = rx.await;
780 }
781 let delay = self.put_whole_delay_ms.load(Ordering::SeqCst);
782 if delay > 0 {
783 tokio::time::sleep(std::time::Duration::from_millis(delay)).await;
784 }
785 let actual = crc64_nvme(&body);
786 if actual != crc64 {
787 return Err(StoreError::ChecksumMismatch(format!(
788 "put {}: claimed {:#x}, content is {:#x}",
789 key, crc64, actual
790 )));
791 }
792 let mut inner = self.inner.lock().unwrap();
793 self.check_condition(inner.current(key), condition)?;
794 let obj = StoredObject {
795 etag: put_etag(&body),
796 crc64,
797 meta: stamps.to_meta().into_iter().collect(),
798 last_modified_unix: now_unix(),
799 bytes: body,
800 version_id: String::new(),
801 deleted: false,
802 };
803 let vid = inner.push(key, obj);
808 let mut m = inner.current(key).expect("just pushed").to_meta();
809 m.version_id = Some(vid);
810 if self.strip_version_ids.load(Ordering::SeqCst) {
811 m.version_id = None;
815 }
816 if self.fail_put_after_land_count.load(Ordering::SeqCst) > 0 {
820 let pat = self.fail_put_after_land_key.lock().unwrap().clone();
821 if (pat.is_empty() || key.contains(pat.as_str()))
822 && self
823 .fail_put_after_land_count
824 .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |v| v.checked_sub(1))
825 .is_ok()
826 {
827 return Err(StoreError::Conflict(format!(
828 "injected: the PUT of {key} landed and the response did not"
829 )));
830 }
831 }
832 Ok(m)
833 }
834
835 async fn copy_object(
836 &self,
837 src_key: &str,
838 src_if_match: Option<&str>,
839 dst_key: &str,
840 condition: &PutCondition,
841 stamps: &GenerationStamps,
842 ) -> StoreResult<ObjectMeta> {
843 self.bump("copy_object");
844 let mut inner = self.inner.lock().unwrap();
845 let src = inner
846 .current(src_key)
847 .ok_or_else(|| StoreError::NotFound(src_key.to_string()))?;
848 if let Some(want) = src_if_match {
852 if src.etag != want {
853 return Err(StoreError::PreconditionFailed(format!(
854 "copy source {src_key} is at {} , not {want}",
855 src.etag
856 )));
857 }
858 }
859 let bytes = src.bytes.clone();
860 let crc64 = src.crc64;
866 self.check_condition(inner.current(dst_key), condition)?;
867 let obj = StoredObject {
868 etag: put_etag(&bytes),
869 crc64,
870 meta: stamps.to_meta().into_iter().collect(),
871 last_modified_unix: now_unix(),
872 bytes,
873 version_id: String::new(),
874 deleted: false,
875 };
876 let vid = inner.push(dst_key, obj);
877 let mut m = inner.current(dst_key).expect("just pushed").to_meta();
878 m.version_id = Some(vid);
879 if self.strip_version_ids.load(Ordering::SeqCst) {
880 m.version_id = None;
881 }
882 Ok(m)
883 }
884
885 async fn compose_generation(&self, spec: &ComposeSpec<'_>) -> StoreResult<ObjectMeta> {
886 self.bump("compose_generation");
887 if spec.parts.is_empty() {
888 return Err(StoreError::Other("compose: no parts".into()));
889 }
890 if spec.parts.len() > self.max_parts {
891 return Err(StoreError::Other(format!(
892 "compose: {} parts exceeds the backend maximum {}",
893 spec.parts.len(),
894 self.max_parts
895 )));
896 }
897 let upload_id = self.raw_begin_upload(spec.key);
898 let result = self.compose_inner(spec, &upload_id).await;
902 if result.is_err() && !self.leave_orphan.swap(false, Ordering::SeqCst) {
903 let mut inner = self.inner.lock().unwrap();
904 inner.uploads.remove(&upload_id);
905 }
906 result
907 }
908
909 async fn head(&self, key: &str) -> StoreResult<ObjectMeta> {
910 self.bump("head");
911 if self
912 .fail_head_count
913 .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |c| c.checked_sub(1))
914 .is_ok()
915 {
916 return Err(StoreError::Other("injected head failure".into()));
917 }
918 self.inner
919 .lock()
920 .unwrap()
921 .current(key)
922 .map(|o| o.to_meta())
923 .ok_or_else(|| StoreError::NotFound(key.into()))
924 }
925
926 async fn presign_get(&self, key: &str, ttl_secs: u64) -> StoreResult<String> {
933 self.bump("presign_get");
934 let inner = self.inner.lock().unwrap();
935 if inner.current(key).is_none() {
936 return Err(StoreError::NotFound(key.into()));
937 }
938 Ok(format!("memory://presigned/{key}?ttl={ttl_secs}"))
939 }
940
941 async fn presign_put(
947 &self,
948 key: &str,
949 ttl_secs: u64,
950 sha256: &[u8; 32],
951 ) -> StoreResult<PresignedPut> {
952 self.bump("presign_put");
953 let headers: BTreeMap<String, String> = [
954 ("x-amz-checksum-sha256".to_string(), crate::base64_std(sha256)),
955 ("if-none-match".to_string(), "*".to_string()),
956 ]
957 .into_iter()
958 .collect();
959 let mut url = format!("memory://presigned-put/{key}?ttl={ttl_secs}");
960 for (k, v) in &headers {
961 url.push_str(&format!("&h.{k}={v}"));
962 }
963 Ok(PresignedPut { url, headers })
964 }
965
966 async fn get_whole(
967 &self,
968 key: &str,
969 if_match: Option<&str>,
970 ) -> StoreResult<(ObjectMeta, Bytes)> {
971 self.bump("get_whole");
972 let inner = self.inner.lock().unwrap();
973 let o = inner
974 .current(key)
975 .ok_or_else(|| StoreError::NotFound(key.into()))?;
976 if let Some(want) = if_match {
977 if o.etag != want {
978 return Err(StoreError::PreconditionFailed(format!(
979 "get {}: etag {} != {}",
980 key, o.etag, want
981 )));
982 }
983 }
984 Ok((o.to_meta(), o.bytes.clone()))
985 }
986
987 async fn get_range_segments(
994 &self,
995 key: &str,
996 offset: u64,
997 len: u64,
998 if_match: &str,
999 ) -> StoreResult<Vec<Bytes>> {
1000 let whole = self.get_range(key, offset, len, if_match).await?;
1001 let mut segs: Vec<Bytes> = Vec::new();
1005 let (mut at, mut step) = (0usize, 1usize);
1006 while at < whole.len() {
1007 let n = step.min(whole.len() - at);
1008 segs.push(whole.slice(at..at + n)); at += n;
1010 step = (step.saturating_mul(3)).min(64 * 1024);
1011 }
1012 if segs.is_empty() {
1013 segs.push(whole);
1014 }
1015 Ok(segs)
1016 }
1017
1018 async fn get_range(
1019 &self,
1020 key: &str,
1021 offset: u64,
1022 len: u64,
1023 if_match: &str,
1024 ) -> StoreResult<Bytes> {
1025 self.bump("get_range");
1026 let _in_flight = {
1027 let now = self.inflight_get_range.fetch_add(1, Ordering::SeqCst) + 1;
1028 self.peak_get_range.fetch_max(now, Ordering::SeqCst);
1029 InFlight(&self.inflight_get_range)
1030 };
1031 if self
1033 .fail_get_range_count
1034 .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |c| c.checked_sub(1))
1035 .is_ok()
1036 {
1037 return Err(StoreError::Other("injected get_range failure".into()));
1038 }
1039 let stall = self.stall_next_get_range_ms.swap(0, Ordering::SeqCst);
1040 if stall > 0 {
1041 tokio::time::sleep(std::time::Duration::from_millis(stall)).await;
1042 }
1043 let delay = self.get_range_delay_ms.load(Ordering::SeqCst);
1044 if delay > 0 {
1045 tokio::time::sleep(std::time::Duration::from_millis(delay)).await;
1046 }
1047 let inner = self.inner.lock().unwrap();
1048 let o = inner
1049 .current(key)
1050 .ok_or_else(|| StoreError::NotFound(key.into()))?;
1051 if o.etag != if_match {
1052 return Err(StoreError::PreconditionFailed(format!(
1053 "get_range {}: etag {} != {}",
1054 key, o.etag, if_match
1055 )));
1056 }
1057 let start = offset.min(o.bytes.len() as u64) as usize;
1058 let end = (offset + len).min(o.bytes.len() as u64) as usize;
1059 Ok(o.bytes.slice(start..end))
1060 }
1061
1062 async fn list(&self, prefix: &str) -> StoreResult<Vec<ListedObject>> {
1063 self.bump("list");
1064 Ok(self
1065 .inner
1066 .lock()
1067 .unwrap()
1068 .chains
1069 .range(prefix.to_string()..)
1070 .take_while(|(k, _)| k.starts_with(prefix))
1071 .filter_map(|(k, c)| {
1074 c.current().map(|o| ListedObject {
1075 key: k.clone(),
1076 size: o.bytes.len() as u64,
1077 etag: o.etag.clone(),
1078 last_modified_unix: Some(o.last_modified_unix),
1079 })
1080 })
1081 .collect())
1082 }
1083
1084 async fn delete(&self, key: &str) -> StoreResult<()> {
1085 self.bump("delete");
1086 self.inner.lock().unwrap().push_delete_marker(key);
1087 Ok(())
1088 }
1089
1090 async fn delete_if_match(&self, key: &str, etag: &str) -> StoreResult<()> {
1091 self.bump("delete");
1093 let mut inner = self.inner.lock().unwrap();
1094 let refused = match inner.current(key) {
1095 None => Some(StoreError::NotFound(key.to_string())),
1096 Some(o) if o.etag != etag => Some(StoreError::PreconditionFailed(format!(
1097 "{key}: If-Match {etag} but the current etag is {}",
1098 o.etag
1099 ))),
1100 Some(_) => None,
1101 };
1102 if let Some(e) = refused {
1103 return Err(e);
1104 }
1105 inner.push_delete_marker(key);
1106 Ok(())
1107 }
1108
1109 async fn delete_many(&self, keys: &[String]) -> StoreResult<DeleteManyReport> {
1112 let mut report = DeleteManyReport::default();
1113 for chunk in keys.chunks(1000) {
1114 self.bump("delete");
1115 let mut inner = self.inner.lock().unwrap();
1116 for key in chunk {
1117 inner.push_delete_marker(key);
1118 report.deleted += 1;
1119 }
1120 }
1121 Ok(report)
1122 }
1123
1124 async fn head_version(&self, key: &str, version_id: &str) -> StoreResult<ObjectMeta> {
1125 self.bump("head_version");
1126 self.inner
1127 .lock()
1128 .unwrap()
1129 .version(key, version_id)
1130 .map(|o| o.to_meta())
1131 .ok_or_else(|| StoreError::NotFound(format!("{key}?versionId={version_id}")))
1132 }
1133
1134 async fn get_version(&self, key: &str, version_id: &str) -> StoreResult<(ObjectMeta, Bytes)> {
1135 self.bump("get_version");
1136 let inner = self.inner.lock().unwrap();
1137 let o = inner
1138 .version(key, version_id)
1139 .ok_or_else(|| StoreError::NotFound(format!("{key}?versionId={version_id}")))?;
1140 Ok((o.to_meta(), o.bytes.clone()))
1141 }
1142
1143 async fn delete_version(&self, key: &str, version_id: &str) -> StoreResult<()> {
1144 self.bump("delete_version");
1145 self.inner.lock().unwrap().remove_version(key, version_id);
1148 Ok(())
1149 }
1150
1151 async fn list_versions(&self, prefix: &str) -> StoreResult<Vec<ListedVersion>> {
1152 self.bump("list_versions");
1153 let inner = self.inner.lock().unwrap();
1154 let mut out = vec![];
1155 for (k, chain) in inner.chains.range(prefix.to_string()..) {
1156 if !k.starts_with(prefix) {
1157 break;
1158 }
1159 let last = chain.versions.len().saturating_sub(1);
1160 for (i, o) in chain.versions.iter().enumerate() {
1161 out.push(ListedVersion {
1162 key: k.clone(),
1163 version_id: o.version_id.clone(),
1164 etag: o.etag.clone(),
1165 size: o.bytes.len() as u64,
1166 is_current: i == last,
1167 is_delete_marker: o.deleted,
1168 last_modified_unix: Some(o.last_modified_unix),
1169 });
1170 }
1171 }
1172 Ok(out)
1173 }
1174
1175 async fn list_uploads(&self, prefix: &str) -> StoreResult<Vec<PendingUpload>> {
1176 self.bump("list_uploads");
1177 let inner = self.inner.lock().unwrap();
1178 let mut v: Vec<PendingUpload> = inner
1179 .uploads
1180 .iter()
1181 .filter(|(_, m)| m.key.starts_with(prefix))
1182 .map(|(id, m)| PendingUpload {
1183 key: m.key.clone(),
1184 upload_id: id.clone(),
1185 initiated_unix: Some(m.initiated_unix),
1186 })
1187 .collect();
1188 v.sort_by(|a, b| a.upload_id.cmp(&b.upload_id));
1189 Ok(v)
1190 }
1191
1192 async fn abort_upload(&self, _key: &str, upload_id: &str) -> StoreResult<()> {
1193 self.bump("abort_upload");
1194 self.inner.lock().unwrap().uploads.remove(upload_id);
1196 Ok(())
1197 }
1198
1199 async fn bootstrap(&self, _prefix: &str) -> StoreResult<BootstrapReport> {
1200 self.bump("bootstrap");
1201 Ok(BootstrapReport {
1202 notes: vec!["memory store: no bucket posture to verify".into()],
1203 ..Default::default()
1204 })
1205 }
1206
1207 async fn lifecycle_rules(&self) -> StoreResult<Vec<LifecycleView>> {
1208 self.bump("lifecycle_rules");
1209 Ok(self.inner.lock().unwrap().lifecycle.clone())
1210 }
1211
1212 async fn ensure_noncurrent_retention(
1213 &self,
1214 prefix: &str,
1215 days: u64,
1216 ) -> StoreResult<RetentionOutcome> {
1217 self.bump("ensure_noncurrent_retention");
1218 if self.fail_lifecycle_writes.load(Ordering::SeqCst) {
1219 return Err(StoreError::Other("lifecycle writes are denied".into()));
1220 }
1221 let rule_id = format!("flint-lean-noncurrent-{}", prefix.trim_end_matches('/').replace('/', "-"));
1222 let mut inner = self.inner.lock().unwrap();
1223 if let Some(r) = inner
1226 .lifecycle
1227 .iter()
1228 .find(|r| r.enabled && r.prefix == prefix && r.noncurrent_days == Some(days))
1229 {
1230 return Ok(RetentionOutcome {
1231 rule_id: r.id.clone(),
1232 noncurrent_days: days,
1233 created: false,
1234 });
1235 }
1236 inner.lifecycle.retain(|r| r.id != rule_id);
1237 inner.lifecycle.push(LifecycleView {
1238 id: rule_id.clone(),
1239 enabled: true,
1240 prefix: prefix.to_string(),
1241 noncurrent_days: Some(days),
1242 expired_delete_marker: true,
1243 });
1244 Ok(RetentionOutcome { rule_id, noncurrent_days: days, created: true })
1245 }
1246
1247 async fn epoch_read(&self, key: &str) -> StoreResult<Option<EpochState>> {
1248 self.bump("epoch_read");
1249 let inner = self.inner.lock().unwrap();
1250 let Some(o) = inner.current(key) else {
1251 return Ok(None);
1252 };
1253 let body: EpochBody = serde_json::from_slice(&o.bytes)
1254 .map_err(|e| StoreError::Other(format!("epoch body: {}", e)))?;
1255 Ok(Some(EpochState {
1256 holder_id: body.holder_id,
1257 epoch: body.epoch,
1258 token: o.etag.clone(),
1259 last_renew_unix: Some(o.last_modified_unix),
1260 released: body.released,
1261 echo: body.echo,
1262 waiters: body.waiters,
1263 handoff: body.handoff,
1264 }))
1265 }
1266
1267 async fn epoch_acquire(
1268 &self,
1269 key: &str,
1270 holder_id: &str,
1271 supersede: Option<&EpochState>,
1272 ) -> StoreResult<EpochLease> {
1273 self.bump("epoch_acquire");
1274 let epoch = supersede.map_or(1, |s| s.epoch + 1);
1275 let waiters: Vec<String> = supersede
1276 .map(|s| s.waiters.iter().filter(|w| w.as_str() != holder_id).cloned().collect())
1277 .unwrap_or_default();
1278 let condition = match supersede {
1279 None => PutCondition::IfNoneMatchAny,
1280 Some(s) => PutCondition::IfMatch(s.token.clone()),
1281 };
1282 let token = self.epoch_store(
1283 key,
1284 EpochBody {
1285 holder_id: holder_id.into(),
1286 epoch,
1287 released: false,
1288 echo: None,
1289 waiters: waiters.clone(),
1290 handoff: None,
1291 },
1292 &condition,
1293 )?;
1294 Ok(EpochLease { holder_id: holder_id.into(), epoch, token, waiters })
1295 }
1296
1297 async fn epoch_renew(
1298 &self,
1299 key: &str,
1300 lease: &EpochLease,
1301 echo: Option<&str>,
1302 ) -> StoreResult<EpochLease> {
1303 self.bump("epoch_renew");
1304 if self
1305 .fail_epoch_renew_count
1306 .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |c| c.checked_sub(1))
1307 .is_ok()
1308 {
1309 return Err(StoreError::Other("injected renew failure: store unreachable".into()));
1310 }
1311 let token = self.epoch_store(
1312 key,
1313 EpochBody {
1314 holder_id: lease.holder_id.clone(),
1315 epoch: lease.epoch,
1316 released: false,
1317 echo: echo.map(|e| e.to_string()),
1318 waiters: lease.waiters.clone(),
1319 handoff: None,
1320 },
1321 &PutCondition::IfMatch(lease.token.clone()),
1322 )?;
1323 Ok(EpochLease {
1324 holder_id: lease.holder_id.clone(),
1325 epoch: lease.epoch,
1326 token,
1327 waiters: lease.waiters.clone(),
1328 })
1329 }
1330
1331 async fn epoch_release(&self, key: &str, lease: &EpochLease) -> StoreResult<()> {
1332 self.bump("epoch_release");
1333 self.epoch_store(
1338 key,
1339 EpochBody {
1340 holder_id: lease.holder_id.clone(),
1341 epoch: lease.epoch,
1342 released: true,
1343 echo: None,
1344 waiters: lease.waiters.clone(),
1345 handoff: None,
1346 },
1347 &PutCondition::IfMatch(lease.token.clone()),
1348 )?;
1349 Ok(())
1350 }
1351
1352 async fn epoch_handoff(
1353 &self,
1354 key: &str,
1355 lease: &EpochLease,
1356 echo: Option<&str>,
1357 ) -> StoreResult<()> {
1358 self.bump("epoch_handoff");
1359 let mut rest = lease.waiters.clone();
1360 let head = if rest.is_empty() { None } else { Some(rest.remove(0)) };
1361 self.epoch_store(
1362 key,
1363 EpochBody {
1364 holder_id: lease.holder_id.clone(),
1365 epoch: lease.epoch,
1366 released: true,
1367 echo: echo.map(|e| e.to_string()),
1368 waiters: rest,
1369 handoff: head,
1370 },
1371 &PutCondition::IfMatch(lease.token.clone()),
1372 )?;
1373 Ok(())
1374 }
1375
1376 async fn epoch_enqueue(
1377 &self,
1378 key: &str,
1379 observed: &EpochState,
1380 holder_id: &str,
1381 ) -> StoreResult<EpochState> {
1382 self.bump("epoch_enqueue");
1383 let mut waiters = observed.waiters.clone();
1384 if !waiters.iter().any(|w| w == holder_id) {
1385 waiters.push(holder_id.to_string());
1386 }
1387 let token = self.epoch_store(
1388 key,
1389 EpochBody {
1390 holder_id: observed.holder_id.clone(),
1391 epoch: observed.epoch,
1392 released: observed.released,
1393 echo: observed.echo.clone(),
1394 waiters: waiters.clone(),
1395 handoff: observed.handoff.clone(),
1396 },
1397 &PutCondition::IfMatch(observed.token.clone()),
1398 )?;
1399 Ok(EpochState {
1400 holder_id: observed.holder_id.clone(),
1401 epoch: observed.epoch,
1402 token,
1403 last_renew_unix: Some(now_unix()),
1404 echo: observed.echo.clone(),
1405 released: observed.released,
1406 waiters,
1407 handoff: observed.handoff.clone(),
1408 })
1409 }
1410
1411 fn min_part_size(&self) -> u64 {
1412 self.min_part
1413 }
1414
1415 fn max_parts(&self) -> usize {
1416 self.max_parts
1417 }
1418
1419 fn upload_gate(&self) -> Option<std::sync::Arc<crate::gate::ByteGate>> {
1420 self.upload_gate.clone()
1421 }
1422}
1423
1424impl MemoryStore {
1425 async fn stage_one_part(
1431 &self,
1432 spec: &ComposeSpec<'_>,
1433 upload_id: &str,
1434 i: usize,
1435 p: &PartSource,
1436 ) -> StoreResult<()> {
1437 match p {
1438 PartSource::Local { offset, len } => {
1439 let _permit = match &self.upload_gate {
1448 Some(g) => Some(g.acquire(*len).await),
1449 None => None,
1450 };
1451 let file = spec.local.clone().ok_or_else(|| {
1452 StoreError::Other(format!(
1453 "compose of {}: a Local part with no open descriptor",
1454 spec.key
1455 ))
1456 })?;
1457 let (offset, len) = (*offset, *len);
1458 let bytes = tokio::task::spawn_blocking(move || read_local(&file, offset, len))
1459 .await
1460 .map_err(|e| StoreError::Other(format!("local read join: {e}")))??;
1461 self.land_part(spec, upload_id, i, len, bytes)
1462 }
1463 PartSource::BaseCopy { offset, len } => {
1464 let want = spec.base_etag.as_deref().ok_or_else(|| {
1465 StoreError::Other("compose: BaseCopy without base_etag".into())
1466 })?;
1467 let bytes = {
1468 let inner = self.inner.lock().unwrap();
1469 let base_key = spec.base_key.unwrap_or(spec.key);
1470 let base = inner.current(base_key).ok_or_else(|| {
1471 StoreError::PreconditionFailed("copy-source: no base object".into())
1472 })?;
1473 if base.etag != want {
1474 return Err(StoreError::PreconditionFailed(
1475 "copy-source-if-match: base etag differs".into(),
1476 ));
1477 }
1478 let s = *offset as usize;
1479 let e = (*offset + *len) as usize;
1480 if e > base.bytes.len() {
1481 return Err(StoreError::Other(format!(
1482 "copy-source range [{}, {}) beyond base size {}",
1483 s,
1484 e,
1485 base.bytes.len()
1486 )));
1487 }
1488 base.bytes.slice(s..e)
1489 };
1490 self.land_part(spec, upload_id, i, *len, bytes)
1491 }
1492 }
1493 }
1494
1495 fn land_part(
1498 &self,
1499 spec: &ComposeSpec<'_>,
1500 upload_id: &str,
1501 i: usize,
1502 want_len: u64,
1503 bytes: Bytes,
1504 ) -> StoreResult<()> {
1505 if bytes.len() as u64 != want_len {
1506 return Err(StoreError::Other(format!("compose: part {} short read", i)));
1507 }
1508 spec.note_progress(bytes.len() as u64);
1509 self.inner
1510 .lock()
1511 .unwrap()
1512 .uploads
1513 .get_mut(upload_id)
1514 .ok_or_else(|| StoreError::NoSuchUpload(upload_id.into()))?
1515 .parts
1516 .insert(i, bytes);
1517 Ok(())
1518 }
1519
1520 async fn compose_inner(
1521 &self,
1522 spec: &ComposeSpec<'_>,
1523 upload_id: &str,
1524 ) -> StoreResult<ObjectMeta> {
1525 self.bump("compose_inner");
1526 if spec.crc64.is_none()
1529 && spec.parts.iter().any(|p| matches!(p, PartSource::BaseCopy { .. }))
1530 {
1531 return Err(StoreError::Other(
1532 "compose: the checksum cannot be accumulated over a copied part; supply crc64".into(),
1533 ));
1534 }
1535 let mut expect = 0u64;
1539 for (i, p) in spec.parts.iter().enumerate() {
1540 let (off, len) = match p {
1541 PartSource::Local { offset, len } | PartSource::BaseCopy { offset, len } => {
1542 (*offset, *len)
1543 }
1544 };
1545 if off != expect {
1546 return Err(StoreError::Other(format!(
1547 "compose: part {} at offset {} but object is contiguous to {}",
1548 i, off, expect
1549 )));
1550 }
1551 if len < self.min_part && i + 1 != spec.parts.len() {
1552 return Err(StoreError::Other(format!(
1553 "compose: part {} len {} under the backend minimum {}",
1554 i, len, self.min_part
1555 )));
1556 }
1557 expect = off + len;
1558 }
1559
1560 {
1567 use futures::stream::StreamExt;
1568 let par = self.part_parallelism.max(1);
1569 let mut futs = Vec::with_capacity(spec.parts.len());
1570 for (i, p) in spec.parts.iter().enumerate() {
1571 futs.push(self.stage_one_part(spec, upload_id, i, p));
1572 }
1573 let staged: Vec<StoreResult<()>> =
1574 futures::stream::iter(futs).buffer_unordered(par).collect().await;
1575 staged.into_iter().collect::<StoreResult<Vec<()>>>()?;
1576 }
1577 let mut assembled = Vec::with_capacity(expect as usize);
1579 {
1580 let inner = self.inner.lock().unwrap();
1581 let up = inner
1582 .uploads
1583 .get(upload_id)
1584 .ok_or_else(|| StoreError::NoSuchUpload(upload_id.into()))?;
1585 for i in 0..spec.parts.len() {
1586 let bytes = up.parts.get(&i).ok_or_else(|| {
1587 StoreError::Other(format!("compose: part {} was staged but did not land", i))
1588 })?;
1589 assembled.extend_from_slice(bytes);
1590 }
1591 }
1592
1593 let actual = crc64_nvme(&assembled);
1603 if let Some(claimed) = spec.crc64 {
1604 if actual != claimed {
1605 return Err(StoreError::ChecksumMismatch(format!(
1606 "compose {}: claimed {:#x}, content is {:#x}",
1607 spec.key, claimed, actual
1608 )));
1609 }
1610 }
1611
1612 match self.inject.swap(INJECT_NONE, Ordering::SeqCst) {
1613 INJECT_CRASH_BEFORE_COMPLETE => {
1614 self.leave_orphan.store(true, Ordering::SeqCst);
1618 return Err(StoreError::Other("injected: crash before Complete".into()));
1619 }
1620 INJECT_TORN_COMPLETE => {
1621 self.finish_complete(spec, upload_id, assembled)?;
1623 return Err(StoreError::Other("injected: Complete response lost".into()));
1624 }
1625 INJECT_SWEPT => {
1626 return Err(StoreError::NoSuchUpload(format!("upload {upload_id} (injected: swept)")));
1627 }
1628 _ => {}
1629 }
1630
1631 let meta = self.finish_complete(spec, upload_id, assembled)?;
1632 Ok(meta)
1633 }
1634
1635 fn finish_complete(
1636 &self,
1637 spec: &ComposeSpec<'_>,
1638 upload_id: &str,
1639 assembled: Vec<u8>,
1640 ) -> StoreResult<ObjectMeta> {
1641 let actual = crc64_nvme(&assembled);
1642 if let Some(claimed) = spec.crc64 {
1643 if actual != claimed {
1644 return Err(StoreError::ChecksumMismatch(format!(
1645 "compose {}: claimed {:#x}, assembled is {:#x}",
1646 spec.key, claimed, actual
1647 )));
1648 }
1649 }
1650 let mut inner = self.inner.lock().unwrap();
1651 if inner.uploads.remove(upload_id).is_none() {
1652 return Err(StoreError::NoSuchUpload(upload_id.into()));
1654 }
1655 self.check_condition(inner.current(spec.key), &spec.condition)?;
1656 let parts = spec.parts.len();
1657 let bytes = Bytes::from(assembled);
1658 let obj = StoredObject {
1659 etag: mpu_etag(&bytes, parts),
1660 crc64: actual,
1661 meta: spec.stamps.to_meta().into_iter().collect(),
1662 last_modified_unix: now_unix(),
1663 bytes,
1664 version_id: String::new(),
1665 deleted: false,
1666 };
1667 let vid = inner.push(spec.key, obj);
1668 let mut m = inner.current(spec.key).expect("just pushed").to_meta();
1669 m.version_id = Some(vid);
1670 Ok(m)
1671 }
1672}
1673
1674fn read_local(file: &std::sync::Arc<std::fs::File>, offset: u64, len: u64) -> StoreResult<Bytes> {
1678 use std::os::unix::fs::FileExt;
1679 let mut buf = vec![0u8; len as usize];
1680 file.read_exact_at(&mut buf, offset)
1681 .map_err(|e| StoreError::Other(format!("local read: {}", e)))?;
1682 Ok(Bytes::from(buf))
1683}
1684
1685#[cfg(test)]
1686mod tests {
1687 use super::*;
1688
1689 fn stamps(generation: u64) -> GenerationStamps {
1690 GenerationStamps { generation, epoch: 1, flush_uuid: format!("u-{}", generation), boundary_source: None, posix: None }
1691 }
1692
1693 #[tokio::test]
1699 async fn delete_many_takes_every_key_tolerates_absent_ones_and_costs_one_request_per_batch() {
1700 let s = MemoryStore::new();
1701 let body = Bytes::from_static(b"bytes");
1702 let crc = crc64_nvme(&body);
1703 for k in ["h/a@1", "h/a@2", "h/b@1"] {
1704 s.put_whole(k, body.clone(), &PutCondition::IfNoneMatchAny, &stamps(1), crc).await.unwrap();
1705 }
1706 s.reset_op_counts();
1707 let keys: Vec<String> = ["h/a@1", "h/b@1", "h/never-existed@9"].iter().map(|k| k.to_string()).collect();
1708 let report = s.delete_many(&keys).await.unwrap();
1709 assert_eq!(report, DeleteManyReport { deleted: 3, failed: vec![] });
1710 assert!(matches!(s.head("h/a@1").await, Err(StoreError::NotFound(_))));
1711 assert!(matches!(s.head("h/b@1").await, Err(StoreError::NotFound(_))));
1712 assert!(s.head("h/a@2").await.is_ok(), "a key the batch did not name must stay");
1713 assert_eq!(s.op_counts().get("delete").copied(), Some(1), "one request for a batch under a thousand");
1714
1715 let big: Vec<String> = (0..2001).map(|i| format!("h/x@{i}")).collect();
1716 s.reset_op_counts();
1717 let report = s.delete_many(&big).await.unwrap();
1718 assert_eq!(report.deleted, 2001);
1719 assert_eq!(s.op_counts().get("delete").copied(), Some(3), "a thousand keys per request");
1720 }
1721
1722 #[tokio::test]
1723 async fn conditional_puts_and_gets_enforce_the_contract() {
1724 let s = MemoryStore::new();
1725 let body = Bytes::from_static(b"gen-1");
1726 let crc = crc64_nvme(&body);
1727
1728 let err = s
1730 .put_whole("k", body.clone(), &PutCondition::IfNoneMatchAny, &stamps(1), crc ^ 1)
1731 .await
1732 .unwrap_err();
1733 assert!(matches!(err, StoreError::ChecksumMismatch(_)));
1734
1735 let m1 = s
1736 .put_whole("k", body.clone(), &PutCondition::IfNoneMatchAny, &stamps(1), crc)
1737 .await
1738 .unwrap();
1739 let err = s
1741 .put_whole("k", body.clone(), &PutCondition::IfNoneMatchAny, &stamps(1), crc)
1742 .await
1743 .unwrap_err();
1744 assert!(matches!(err, StoreError::PreconditionFailed(_)));
1745
1746 let b2 = Bytes::from_static(b"gen-2");
1748 let m2 = s
1749 .put_whole("k", b2.clone(), &PutCondition::IfMatch(m1.etag.clone()), &stamps(2), crc64_nvme(&b2))
1750 .await
1751 .unwrap();
1752 let err = s
1753 .put_whole("k", b2.clone(), &PutCondition::IfMatch(m1.etag.clone()), &stamps(3), crc64_nvme(&b2))
1754 .await
1755 .unwrap_err();
1756 assert!(matches!(err, StoreError::PreconditionFailed(_)));
1757
1758 let (gm, gb) = s.get_whole("k", Some(&m2.etag)).await.unwrap();
1760 assert_eq!(gb, b2);
1761 assert_eq!(GenerationStamps::from_meta(&gm.meta).unwrap().generation, 2);
1762 let err = s.get_whole("k", Some(&m1.etag)).await.unwrap_err();
1763 assert!(matches!(err, StoreError::PreconditionFailed(_)));
1764 }
1765
1766 #[tokio::test]
1767 async fn epoch_cas_lease_lifecycle() {
1768 let s = MemoryStore::new();
1769 const K: &str = "vol/.flint-epoch";
1770
1771 let l1 = s.epoch_acquire(K, "hub-a", None).await.unwrap();
1773 assert_eq!(l1.epoch, 1);
1774 let err = s.epoch_acquire(K, "hub-b", None).await.unwrap_err();
1775 assert!(matches!(err, StoreError::PreconditionFailed(_)));
1776
1777 let l1b = s.epoch_renew(K, &l1, None).await.unwrap();
1780 assert_ne!(l1b.token, l1.token, "renew must rotate the CAS token");
1781 let err = s.epoch_renew(K, &l1, None).await.unwrap_err();
1782 assert!(matches!(err, StoreError::PreconditionFailed(_)), "stale token must be dead");
1783
1784 let observed = s.epoch_read(K).await.unwrap().unwrap();
1788 assert_eq!(observed.holder_id, "hub-a");
1789 let l2 = s.epoch_acquire(K, "hub-b", Some(&observed)).await.unwrap();
1790 assert_eq!(l2.epoch, 2);
1791 let err = s.epoch_renew(K, &l1b, None).await.unwrap_err();
1792 assert!(matches!(err, StoreError::PreconditionFailed(_)), "deposed renew must fail");
1793 let err = s.epoch_release(K, &l1b).await.unwrap_err();
1794 assert!(matches!(err, StoreError::PreconditionFailed(_)), "deposed release must fail");
1795
1796 s.epoch_release(K, &l2).await.unwrap();
1800 let after = s.epoch_read(K).await.unwrap().expect("the cell must survive a release");
1801 assert!(after.released, "a clean shutdown marks the cell released");
1802 assert_eq!(after.epoch, 2, "and the epoch number is preserved");
1803 assert_eq!(after.holder_id, "hub-b");
1804
1805 let l3 = s.epoch_acquire(K, "hub-c", Some(&after)).await.unwrap();
1808 assert_eq!(l3.epoch, 3, "numbering continues, it does not reset");
1809 assert!(
1810 !s.epoch_read(K).await.unwrap().unwrap().released,
1811 "a fresh claim clears the released mark"
1812 );
1813 }
1814
1815 #[tokio::test]
1823 async fn epoch_queue_hands_the_cell_to_the_head_and_keeps_the_echo() {
1824 const K: &str = "t/.flint/lean/epoch";
1825 let s = MemoryStore::new();
1826 let a = s.epoch_acquire(K, "a", None).await.unwrap();
1827 let seen = s.epoch_read(K).await.unwrap().unwrap();
1828 let seen = s.epoch_enqueue(K, &seen, "b").await.unwrap();
1829 let seen = s.epoch_enqueue(K, &seen, "c").await.unwrap();
1830 let seen = s.epoch_enqueue(K, &seen, "b").await.unwrap();
1831 assert_eq!(seen.waiters, vec!["b", "c"], "a waiter is appended once, in order");
1832 let err = s.epoch_renew(K, &a, None).await.unwrap_err();
1835 assert!(matches!(err, StoreError::PreconditionFailed(_)));
1836 let adopted = EpochLease {
1839 holder_id: "a".into(),
1840 epoch: seen.epoch,
1841 token: seen.token.clone(),
1842 waiters: seen.waiters.clone(),
1843 };
1844 let a = s.epoch_renew(K, &adopted, Some("echo-a")).await.unwrap();
1845 assert_eq!(s.epoch_read(K).await.unwrap().unwrap().waiters, vec!["b", "c"]);
1846 s.epoch_handoff(K, &a, Some("echo-a")).await.unwrap();
1848 let cell = s.epoch_read(K).await.unwrap().unwrap();
1849 assert!(cell.released);
1850 assert_eq!(cell.handoff.as_deref(), Some("b"));
1851 assert_eq!(cell.waiters, vec!["c"]);
1852 assert_eq!(cell.echo.as_deref(), Some("echo-a"), "the last barrier's echo survives the handoff");
1853 let b = s.epoch_acquire(K, "b", Some(&cell)).await.unwrap();
1855 assert_eq!(b.epoch, cell.epoch + 1);
1856 assert_eq!(b.waiters, vec!["c"]);
1857 let cell = s.epoch_read(K).await.unwrap().unwrap();
1858 assert!(!cell.released && cell.handoff.is_none());
1859 assert_eq!(cell.holder_id, "b");
1860 assert_eq!(cell.waiters, vec!["c"]);
1861 s.epoch_handoff(K, &b, None).await.unwrap();
1864 let cell = s.epoch_read(K).await.unwrap().unwrap();
1865 assert_eq!(cell.handoff.as_deref(), Some("c"));
1866 assert!(cell.waiters.is_empty());
1867 let c = s.epoch_acquire(K, "c", Some(&cell)).await.unwrap();
1868 assert!(c.waiters.is_empty());
1869 s.epoch_handoff(K, &c, None).await.unwrap();
1870 let cell = s.epoch_read(K).await.unwrap().unwrap();
1871 assert!(cell.released && cell.handoff.is_none() && cell.waiters.is_empty());
1872 }
1873
1874 #[tokio::test]
1875 async fn compose_mixes_local_and_guarded_base_copy() {
1876 let s = MemoryStore::new();
1877 let base_body = Bytes::from_static(b"AAAABBBB");
1879 let base = s
1880 .put_whole("f", base_body.clone(), &PutCondition::IfNoneMatchAny, &stamps(1), crc64_nvme(&base_body))
1881 .await
1882 .unwrap();
1883 let dir = tempfile::TempDir::new().unwrap();
1885 let local = dir.path().join("f.local");
1886 std::fs::write(&local, b"XXXXBBBB").unwrap();
1887
1888 let spec = ComposeSpec {
1889 progress: None,
1890 key: "f",
1891 local: Some(std::sync::Arc::new(std::fs::File::open(&local).unwrap())),
1892 parts: vec![
1893 PartSource::Local { offset: 0, len: 4 },
1894 PartSource::BaseCopy { offset: 4, len: 4 },
1895 ],
1896 base_key: None,
1897 base_etag: Some(base.etag.clone()),
1898 condition: PutCondition::IfMatch(base.etag.clone()),
1899 stamps: stamps(2),
1900 crc64: Some(crc64_nvme(b"XXXXBBBB")),
1901 };
1902 let m2 = s.compose_generation(&spec).await.unwrap();
1903 let (_, bytes) = s.get_whole("f", Some(&m2.etag)).await.unwrap();
1904 assert_eq!(bytes, Bytes::from_static(b"XXXXBBBB"));
1905 assert!(m2.etag.ends_with("-2\""), "multipart etag must not look like a content hash");
1906
1907 s.raw_put("f", Bytes::from_static(b"foreignXX"), vec![]);
1910 let err = s.compose_generation(&spec).await.unwrap_err();
1911 assert!(matches!(err, StoreError::PreconditionFailed(_)));
1912 assert!(
1913 s.list_uploads("f").await.unwrap().is_empty(),
1914 "a failed compose must abort its MPU (A9)"
1915 );
1916 }
1917 #[tokio::test]
1922 async fn compose_without_a_claimed_checksum_publishes_the_content_crc() {
1923 let s = MemoryStore::new();
1924 let dir = tempfile::TempDir::new().unwrap();
1925 let local = dir.path().join("g.local");
1926 let content: Vec<u8> = (0..24u8).collect();
1927 std::fs::write(&local, &content).unwrap();
1928 let spec = ComposeSpec {
1929 progress: None,
1930 key: "g",
1931 local: Some(std::sync::Arc::new(std::fs::File::open(&local).unwrap())),
1932 parts: vec![
1933 PartSource::Local { offset: 0, len: 8 },
1934 PartSource::Local { offset: 8, len: 8 },
1935 PartSource::Local { offset: 16, len: 8 },
1936 ],
1937 base_key: None,
1938 base_etag: None,
1939 condition: PutCondition::IfNoneMatchAny,
1940 stamps: stamps(1),
1941 crc64: None,
1942 };
1943 let m = s.compose_generation(&spec).await.unwrap();
1944 assert_eq!(m.crc64_b64.as_deref(), Some(crc64_to_b64(crc64_nvme(&content)).as_str()));
1945 let (h, bytes) = s.get_whole("g", None).await.unwrap();
1946 assert_eq!(&bytes[..], &content[..]);
1947 assert_eq!(h.crc64_b64, m.crc64_b64);
1948
1949 let base_body = Bytes::from_static(b"AAAABBBB");
1950 let base = s
1951 .put_whole("h", base_body.clone(), &PutCondition::IfNoneMatchAny, &stamps(1), crc64_nvme(&base_body))
1952 .await
1953 .unwrap();
1954 std::fs::write(&local, b"XXXXBBBB").unwrap();
1955 let err = s
1956 .compose_generation(&ComposeSpec {
1957 progress: None,
1958 key: "h",
1959 local: Some(std::sync::Arc::new(std::fs::File::open(&local).unwrap())),
1960 parts: vec![
1961 PartSource::Local { offset: 0, len: 4 },
1962 PartSource::BaseCopy { offset: 4, len: 4 },
1963 ],
1964 base_key: None,
1965 base_etag: Some(base.etag.clone()),
1966 condition: PutCondition::IfMatch(base.etag.clone()),
1967 stamps: stamps(2),
1968 crc64: None,
1969 })
1970 .await
1971 .unwrap_err();
1972 assert!(matches!(err, StoreError::Other(ref m) if m.contains("copied part")), "{err:?}");
1973 assert!(s.list_uploads("h").await.unwrap().is_empty(), "nothing pending after a refusal");
1974 }
1975}
1976
1977#[cfg(test)]
1978mod copy_tests {
1979 use super::*;
1980 use crate::{crc64_from_b64, crc64_to_b64};
1981
1982 fn stamps(g: u64) -> GenerationStamps {
1983 GenerationStamps {
1984 generation: g,
1985 epoch: 7,
1986 flush_uuid: format!("flush-{g}"),
1987 boundary_source: None,
1988 posix: None,
1989 }
1990 }
1991
1992 #[tokio::test]
1993 async fn a_copy_carries_the_bytes_and_the_checksum_but_not_the_stamps() {
1994 let s = MemoryStore::new();
1995 let body = Bytes::from_static(b"the bytes that move");
1996 let src = s
1997 .put_whole("a.txt", body.clone(), &PutCondition::IfNoneMatchAny, &stamps(3), crc64_nvme(&body))
1998 .await
1999 .unwrap();
2000
2001 let dst = s
2002 .copy_object("a.txt", Some(&src.etag), "b.txt", &PutCondition::IfNoneMatchAny, &stamps(1))
2003 .await
2004 .unwrap();
2005
2006 let (meta, got) = s.get_whole("b.txt", None).await.unwrap();
2007 assert_eq!(got, body, "a copy is byte-identical or it is not a copy");
2008 assert_eq!(
2009 dst.crc64_b64, src.crc64_b64,
2010 "identical bytes have an identical checksum — a copy cannot change one"
2011 );
2012 assert_eq!(
2015 meta.meta.get("flint-gen").map(String::as_str),
2016 Some("1"),
2017 "the destination's generation is its OWN, not the source's 3: {:?}",
2018 meta.meta
2019 );
2020 assert_eq!(
2021 meta.meta.get(GenerationStamps::META_FLUSH_UUID).map(String::as_str),
2022 Some("flush-1"),
2023 "and its flush_uuid names the publish that WROTE IT, not flush-3: {:?}",
2024 meta.meta
2025 );
2026 assert!(s.head("a.txt").await.is_ok());
2028 }
2029
2030 #[tokio::test]
2031 async fn a_copy_refuses_when_the_source_moved_under_it() {
2032 let s = MemoryStore::new();
2033 let v1 = Bytes::from_static(b"version one");
2034 let src = s
2035 .put_whole("a.txt", v1, &PutCondition::IfNoneMatchAny, &stamps(1), crc64_nvme(b"version one"))
2036 .await
2037 .unwrap();
2038 let v2 = Bytes::from_static(b"version two");
2039 s.put_whole("a.txt", v2, &PutCondition::IfMatch(src.etag.clone()), &stamps(2), crc64_nvme(b"version two"))
2040 .await
2041 .unwrap();
2042
2043 let err = s
2044 .copy_object("a.txt", Some(&src.etag), "b.txt", &PutCondition::IfNoneMatchAny, &stamps(1))
2045 .await
2046 .expect_err("copying an object that moved copies something the caller never read");
2047 assert!(matches!(err, StoreError::PreconditionFailed(_)), "{err:?}");
2048 assert!(matches!(s.head("b.txt").await, Err(StoreError::NotFound(_))), "and nothing landed");
2049 }
2050
2051 #[tokio::test]
2052 async fn a_copy_honours_the_destination_precondition() {
2053 let s = MemoryStore::new();
2054 let a = Bytes::from_static(b"aaaa");
2055 s.put_whole("a.txt", a, &PutCondition::IfNoneMatchAny, &stamps(1), crc64_nvme(b"aaaa"))
2056 .await
2057 .unwrap();
2058 let b = Bytes::from_static(b"bbbb");
2059 let existing = s
2060 .put_whole("b.txt", b.clone(), &PutCondition::IfNoneMatchAny, &stamps(1), crc64_nvme(b"bbbb"))
2061 .await
2062 .unwrap();
2063
2064 let err = s
2065 .copy_object("a.txt", None, "b.txt", &PutCondition::IfNoneMatchAny, &stamps(2))
2066 .await
2067 .expect_err("a rename must not silently clobber its destination");
2068 assert!(matches!(err, StoreError::PreconditionFailed(_)), "{err:?}");
2069 let (_, still) = s.get_whole("b.txt", Some(&existing.etag)).await.unwrap();
2070 assert_eq!(still, b, "the destination is untouched by a refused copy");
2071 }
2072
2073 #[tokio::test]
2078 async fn a_compose_can_copy_ranges_from_a_different_key() {
2079 let s = MemoryStore::new();
2080 let body = Bytes::from_static(b"AAAABBBB");
2081 let src = s
2082 .put_whole("old/name", body.clone(), &PutCondition::IfNoneMatchAny, &stamps(1), crc64_nvme(&body))
2083 .await
2084 .unwrap();
2085
2086 let spec = ComposeSpec {
2087 progress: None,
2088 key: "new/name",
2089 local: None,
2091 parts: vec![PartSource::BaseCopy { offset: 0, len: 8 }],
2092 base_key: Some("old/name"),
2093 base_etag: Some(src.etag.clone()),
2094 condition: PutCondition::IfNoneMatchAny,
2095 stamps: stamps(1),
2096 crc64: Some(crc64_nvme(&body)),
2097 };
2098 let m = s.compose_generation(&spec).await.unwrap();
2099 let (_, got) = s.get_whole("new/name", Some(&m.etag)).await.unwrap();
2100 assert_eq!(got, body, "the ranges came from the OTHER key");
2101 assert!(s.head("old/name").await.is_ok(), "and the source survives");
2102 }
2103
2104 #[test]
2105 fn crc64_b64_round_trips_and_refuses_what_is_not_one() {
2106 for v in [0u64, 1, u64::MAX, 0x0123_4567_89ab_cdef] {
2107 assert_eq!(crc64_from_b64(&crc64_to_b64(v)), Some(v), "round trip {v:#x}");
2108 }
2109 for bad in ["", "AAAA", "not-base64!!", "AAAAAAAAAAAA", "AAAAAAAAAAA", "AAAAAAAAAAAAAAA="] {
2118 assert_eq!(crc64_from_b64(bad), None, "{bad:?} is not a canonical CRC-64");
2119 }
2120 assert_eq!(crc64_from_b64("AAAAAAAAAAA="), Some(0), "the canonical form IS accepted");
2121 }
2122}