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;
166
167pub struct MemoryStore {
168 inner: Mutex<Inner>,
169 upload_seq: AtomicU64,
170 inject: AtomicU8,
172 leave_orphan: AtomicBool,
175 fail_get_range_count: AtomicU64,
177 fail_head_count: AtomicU64,
179 fail_epoch_renew_count: AtomicU64,
182 fail_put_after_land_count: AtomicU64,
185 fail_put_after_land_key: Mutex<String>,
186 stall_next_get_range_ms: AtomicU64,
187 get_range_delay_ms: AtomicU64,
191 put_whole_delay_ms: AtomicU64,
195 hold_puts: AtomicBool,
200 held_puts: Mutex<Vec<tokio::sync::oneshot::Sender<()>>>,
201 inflight_get_range: AtomicU64,
204 peak_get_range: AtomicU64,
205 strip_version_ids: AtomicBool,
210 fail_lifecycle_writes: AtomicBool,
214 if_match_missing_412: AtomicBool,
221 ops: Mutex<std::collections::BTreeMap<&'static str, u64>>,
225 pub min_part: u64,
226 pub max_parts: usize,
227 part_parallelism: usize,
231 upload_gate: Option<std::sync::Arc<crate::gate::ByteGate>>,
235}
236
237struct InFlight<'a>(&'a AtomicU64);
240impl Drop for InFlight<'_> {
241 fn drop(&mut self) {
242 self.0.fetch_sub(1, Ordering::SeqCst);
243 }
244}
245
246impl Default for MemoryStore {
247 fn default() -> Self {
248 Self::new()
249 }
250}
251
252impl MemoryStore {
253 pub fn backdate_epoch(&self, key: &str, secs: u64) {
257 let mut inner = self.inner.lock().unwrap();
258 if let Some(chain) = inner.chains.get_mut(key) {
259 for v in chain.versions.iter_mut() {
260 v.last_modified_unix = v.last_modified_unix.saturating_sub(secs);
261 }
262 }
263 }
264
265 pub fn plant_lifecycle_rule(&self, rule: LifecycleView) {
268 self.inner.lock().unwrap().lifecycle.push(rule);
269 }
270
271 pub fn new() -> Self {
272 MemoryStore {
273 ops: Mutex::new(Default::default()),
274 inner: Mutex::new(Inner::default()),
275 upload_seq: AtomicU64::new(1),
276 inject: AtomicU8::new(INJECT_NONE),
277 leave_orphan: AtomicBool::new(false),
278 fail_get_range_count: AtomicU64::new(0),
279 fail_head_count: AtomicU64::new(0),
280 fail_epoch_renew_count: AtomicU64::new(0),
281 fail_put_after_land_count: AtomicU64::new(0),
282 fail_put_after_land_key: Mutex::new(String::new()),
283 stall_next_get_range_ms: AtomicU64::new(0),
284 get_range_delay_ms: AtomicU64::new(0),
285 put_whole_delay_ms: AtomicU64::new(0),
286 hold_puts: AtomicBool::new(false),
287 held_puts: Mutex::new(Vec::new()),
288 inflight_get_range: AtomicU64::new(0),
289 peak_get_range: AtomicU64::new(0),
290 strip_version_ids: AtomicBool::new(false),
291 fail_lifecycle_writes: AtomicBool::new(false),
292 if_match_missing_412: AtomicBool::new(false),
293 min_part: 1,
296 max_parts: 10_000,
297 part_parallelism: 1,
298 upload_gate: None,
299 }
300 }
301
302 pub fn with_if_match_missing_as_412(self) -> Self {
305 self.if_match_missing_412.store(true, Ordering::SeqCst);
306 self
307 }
308
309 pub fn with_part_parallelism(mut self, n: usize) -> Self {
312 self.part_parallelism = n.max(1);
313 self
314 }
315
316 pub fn with_upload_inflight_max_bytes(mut self, n: u64) -> Self {
319 self.upload_gate = (n > 0).then(|| std::sync::Arc::new(crate::gate::ByteGate::new(n)));
320 self
321 }
322
323 pub fn inject_put_whole_delay_ms(&self, ms: u64) {
329 self.put_whole_delay_ms.store(ms, Ordering::SeqCst);
330 }
331
332 pub fn strip_version_ids(&self, on: bool) {
335 self.strip_version_ids.store(on, Ordering::SeqCst);
336 }
337
338 pub fn fail_lifecycle_writes(&self, on: bool) {
340 self.fail_lifecycle_writes.store(on, Ordering::SeqCst);
341 }
342
343 pub fn inject_torn_complete(&self) {
345 self.inject.store(INJECT_TORN_COMPLETE, Ordering::SeqCst);
346 }
347
348 pub fn inject_crash_before_complete(&self) {
350 self.inject.store(INJECT_CRASH_BEFORE_COMPLETE, Ordering::SeqCst);
351 }
352
353 pub fn inject_get_range_failure(&self) {
356 self.inject_get_range_failures(1);
357 }
358
359 pub fn inject_get_range_failures(&self, n: u64) {
362 self.fail_get_range_count.store(n, Ordering::SeqCst);
363 }
364
365 pub fn inject_get_range_delay_ms(&self, ms: u64) {
371 self.get_range_delay_ms.store(ms, Ordering::SeqCst);
372 }
373
374 pub fn inject_put_hold(&self, on: bool) {
380 self.hold_puts.store(on, Ordering::SeqCst);
381 }
382
383 pub fn held_puts(&self) -> usize {
385 self.held_puts.lock().unwrap().len()
386 }
387
388 pub fn release_held_puts(&self) {
390 for tx in self.held_puts.lock().unwrap().drain(..) {
391 let _ = tx.send(());
392 }
393 }
394
395 pub fn peak_get_range_in_flight(&self) -> u64 {
399 self.peak_get_range.load(Ordering::SeqCst)
400 }
401 pub fn reset_peak_get_range_in_flight(&self) {
402 self.peak_get_range.store(0, Ordering::SeqCst);
403 }
404
405 pub fn inject_head_failures(&self, n: u64) {
412 self.fail_head_count.store(n, Ordering::SeqCst);
413 }
414
415 pub fn inject_epoch_renew_failures(&self, n: u64) {
418 self.fail_epoch_renew_count.store(n, Ordering::SeqCst);
419 }
420
421 pub fn inject_put_lands_then_fails(&self, key_contains: &str, n: u64) {
434 *self.fail_put_after_land_key.lock().unwrap() = key_contains.to_string();
435 self.fail_put_after_land_count.store(n, Ordering::SeqCst);
436 }
437
438 pub fn inject_get_range_stall(&self, ms: u64) {
441 self.stall_next_get_range_ms.store(ms, Ordering::SeqCst);
442 }
443
444 pub fn inject_corrupt_body(&self, key: &str, mutate: impl FnOnce(&mut Vec<u8>)) {
451 let mut inner = self.inner.lock().unwrap();
452 let chain = inner.chains.get_mut(key).expect("corrupting a key that exists");
453 let obj = chain.versions.last_mut().expect("a version to corrupt");
454 let mut b = obj.bytes.to_vec();
455 mutate(&mut b);
456 obj.bytes = Bytes::from(b);
457 }
458
459 pub fn raw_put(&self, key: &str, bytes: Bytes, meta: Vec<(String, String)>) -> ObjectMeta {
462 let obj = StoredObject {
463 etag: put_etag(&bytes),
464 crc64: crc64_nvme(&bytes),
465 meta: meta.into_iter().collect(),
466 last_modified_unix: now_unix(),
467 bytes,
468 version_id: String::new(),
469 deleted: false,
470 };
471 let mut inner = self.inner.lock().unwrap();
472 let vid = inner.push(key, obj);
473 let mut m = inner.current(key).map(|o| o.to_meta()).expect("just pushed");
474 m.version_id = Some(vid);
475 m
476 }
477
478 pub fn expire_noncurrent(&self, older_than_secs: u64) -> Vec<(String, String)> {
494 let now = now_unix();
495 let mut inner = self.inner.lock().unwrap();
496 let mut reaped = vec![];
497 let keys: Vec<String> = inner.chains.keys().cloned().collect();
498 for k in keys {
499 let Some(chain) = inner.chains.get_mut(&k) else { continue };
500 let last = chain.versions.len().saturating_sub(1);
501 let doomed: Vec<String> = chain
502 .versions
503 .iter()
504 .enumerate()
505 .filter(|(i, o)| {
506 *i != last && now.saturating_sub(o.last_modified_unix) >= older_than_secs
507 })
508 .map(|(_, o)| o.version_id.clone())
509 .collect();
510 for vid in doomed {
511 chain.versions.retain(|o| o.version_id != vid);
512 reaped.push((k.clone(), vid));
513 }
514 }
515 reaped
516 }
517
518 pub fn version_count(&self, key: &str) -> usize {
523 self.inner.lock().unwrap().chains.get(key).map(|c| c.versions.len()).unwrap_or(0)
524 }
525
526 pub fn raw_complete_upload(&self, upload_id: &str) -> StoreResult<ObjectMeta> {
532 let mut inner = self.inner.lock().unwrap();
533 let Some(mpu) = inner.uploads.remove(upload_id) else {
534 return Err(StoreError::NoSuchUpload(format!("upload {}", upload_id)));
535 };
536 let mut bytes = Vec::new();
537 let n_parts = mpu.parts.len().max(1);
538 for part in mpu.parts.values() {
539 bytes.extend_from_slice(part);
540 }
541 let bytes = Bytes::from(bytes);
542 let obj = StoredObject {
543 etag: mpu_etag(&bytes, n_parts),
544 crc64: crc64_nvme(&bytes),
545 meta: HashMap::new(),
546 last_modified_unix: now_unix(),
547 bytes,
548 version_id: String::new(),
549 deleted: false,
550 };
551 let mkey = mpu.key.clone();
552 let vid = inner.push(&mkey, obj);
553 let mut m = inner.current(&mkey).expect("just pushed").to_meta();
554 m.version_id = Some(vid);
555 Ok(m)
556 }
557
558 pub fn raw_begin_upload(&self, key: &str) -> String {
560 let id = format!("mpu-{}", self.upload_seq.fetch_add(1, Ordering::SeqCst));
561 self.inner.lock().unwrap().uploads.insert(
562 id.clone(),
563 Mpu { key: key.to_string(), parts: BTreeMap::new(), initiated_unix: now_unix() },
564 );
565 id
566 }
567
568 fn check_condition(
569 &self,
570 existing: Option<&StoredObject>,
571 condition: &PutCondition,
572 ) -> StoreResult<()> {
573 match (condition, existing) {
574 (PutCondition::IfMatch(want), Some(o)) if o.etag == *want => Ok(()),
575 (PutCondition::IfMatch(_), Some(_)) => {
576 Err(StoreError::PreconditionFailed("If-Match: etag differs".into()))
577 }
578 (PutCondition::IfMatch(_), None) if self.if_match_missing_412.load(Ordering::SeqCst) => {
579 Err(StoreError::PreconditionFailed("If-Match: no object".into()))
580 }
581 (PutCondition::IfMatch(_), None) => Err(StoreError::NotFound("NoSuchKey: If-Match on a missing key".into())),
582 (PutCondition::IfNoneMatchAny, None) => Ok(()),
583 (PutCondition::IfNoneMatchAny, Some(_)) => {
584 Err(StoreError::PreconditionFailed("If-None-Match: object exists".into()))
585 }
586 (PutCondition::Unconditional, _) => Ok(()),
587 }
588 }
589}
590
591fn put_etag(bytes: &[u8]) -> String {
592 let mut h = Sha256::new();
593 h.update(bytes);
594 format!("\"{:x}\"", h.finalize())
595}
596
597fn mpu_etag(bytes: &[u8], parts: usize) -> String {
601 let mut h = Sha256::new();
602 h.update(bytes);
603 format!("\"{:x}-{}\"", h.finalize(), parts)
604}
605
606#[derive(serde::Serialize, serde::Deserialize)]
607struct EpochBody {
608 holder_id: String,
609 epoch: u64,
610 #[serde(default)]
612 released: bool,
613 #[serde(default, skip_serializing_if = "Option::is_none")]
615 echo: Option<String>,
616 #[serde(default, skip_serializing_if = "Vec::is_empty")]
618 waiters: Vec<String>,
619 #[serde(default, skip_serializing_if = "Option::is_none")]
620 handoff: Option<String>,
621}
622
623impl MemoryStore {
624 fn epoch_store(&self, key: &str, body: EpochBody, condition: &PutCondition) -> StoreResult<String> {
629 let bytes = Bytes::from(serde_json::to_vec(&body).unwrap());
630 let mut inner = self.inner.lock().unwrap();
631 self.check_condition(inner.current(key), condition)?;
632 let mut salted = bytes.to_vec();
633 salted.extend_from_slice(&self.upload_seq.fetch_add(1, Ordering::SeqCst).to_be_bytes());
634 let obj = StoredObject {
635 etag: put_etag(&salted),
636 crc64: crc64_nvme(&bytes),
637 meta: HashMap::new(),
638 last_modified_unix: now_unix(),
639 bytes,
640 version_id: String::new(),
641 deleted: false,
642 };
643 let token = obj.etag.clone();
644 inner.push(key, obj);
645 Ok(token)
646 }
647
648 fn bump(&self, op: &'static str) {
649 *self.ops.lock().unwrap().entry(op).or_insert(0) += 1;
650 }
651 pub fn op_counts(&self) -> std::collections::BTreeMap<&'static str, u64> {
653 self.ops.lock().unwrap().clone()
654 }
655 pub fn reset_op_counts(&self) {
656 self.ops.lock().unwrap().clear();
657 }
658 pub fn total_ops(&self) -> u64 {
659 self.ops.lock().unwrap().values().sum()
660 }
661}
662
663#[async_trait]
664impl ObjectStore for MemoryStore {
665 fn request_counts(&self) -> Option<crate::RequestCounts> {
666 let mut c = crate::RequestCounts::default();
669 for (op, n) in self.op_counts() {
670 match op {
671 "get_whole" | "get_range" | "get_version" | "epoch_read" | "presign_get" => c.get += n,
672 "head" | "head_version" => c.head += n,
673 "put_whole" | "epoch_acquire" | "epoch_renew" | "epoch_release" | "epoch_handoff"
674 | "epoch_enqueue" | "presign_put" => c.put += n,
675 "copy_object" => c.copy += n,
676 "delete" | "delete_version" => c.delete += n,
677 "list" | "list_versions" | "list_uploads" => c.list += n,
678 "compose_generation" | "compose_inner" | "abort_upload" => c.multipart += n,
679 _ => {}
680 }
681 }
682 Some(c)
683 }
684
685 async fn put_whole(
686 &self,
687 key: &str,
688 body: Bytes,
689 condition: &PutCondition,
690 stamps: &GenerationStamps,
691 crc64: u64,
692 ) -> StoreResult<ObjectMeta> {
693 self.bump("put_whole");
694 if self.hold_puts.load(Ordering::SeqCst) {
695 let (tx, rx) = tokio::sync::oneshot::channel();
696 self.held_puts.lock().unwrap().push(tx);
697 let _ = rx.await;
700 }
701 let delay = self.put_whole_delay_ms.load(Ordering::SeqCst);
702 if delay > 0 {
703 tokio::time::sleep(std::time::Duration::from_millis(delay)).await;
704 }
705 let actual = crc64_nvme(&body);
706 if actual != crc64 {
707 return Err(StoreError::ChecksumMismatch(format!(
708 "put {}: claimed {:#x}, content is {:#x}",
709 key, crc64, actual
710 )));
711 }
712 let mut inner = self.inner.lock().unwrap();
713 self.check_condition(inner.current(key), condition)?;
714 let obj = StoredObject {
715 etag: put_etag(&body),
716 crc64,
717 meta: stamps.to_meta().into_iter().collect(),
718 last_modified_unix: now_unix(),
719 bytes: body,
720 version_id: String::new(),
721 deleted: false,
722 };
723 let vid = inner.push(key, obj);
728 let mut m = inner.current(key).expect("just pushed").to_meta();
729 m.version_id = Some(vid);
730 if self.strip_version_ids.load(Ordering::SeqCst) {
731 m.version_id = None;
735 }
736 if self.fail_put_after_land_count.load(Ordering::SeqCst) > 0 {
740 let pat = self.fail_put_after_land_key.lock().unwrap().clone();
741 if (pat.is_empty() || key.contains(pat.as_str()))
742 && self
743 .fail_put_after_land_count
744 .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |v| v.checked_sub(1))
745 .is_ok()
746 {
747 return Err(StoreError::Conflict(format!(
748 "injected: the PUT of {key} landed and the response did not"
749 )));
750 }
751 }
752 Ok(m)
753 }
754
755 async fn copy_object(
756 &self,
757 src_key: &str,
758 src_if_match: Option<&str>,
759 dst_key: &str,
760 condition: &PutCondition,
761 stamps: &GenerationStamps,
762 ) -> StoreResult<ObjectMeta> {
763 self.bump("copy_object");
764 let mut inner = self.inner.lock().unwrap();
765 let src = inner
766 .current(src_key)
767 .ok_or_else(|| StoreError::NotFound(src_key.to_string()))?;
768 if let Some(want) = src_if_match {
772 if src.etag != want {
773 return Err(StoreError::PreconditionFailed(format!(
774 "copy source {src_key} is at {} , not {want}",
775 src.etag
776 )));
777 }
778 }
779 let bytes = src.bytes.clone();
780 let crc64 = src.crc64;
786 self.check_condition(inner.current(dst_key), condition)?;
787 let obj = StoredObject {
788 etag: put_etag(&bytes),
789 crc64,
790 meta: stamps.to_meta().into_iter().collect(),
791 last_modified_unix: now_unix(),
792 bytes,
793 version_id: String::new(),
794 deleted: false,
795 };
796 let vid = inner.push(dst_key, obj);
797 let mut m = inner.current(dst_key).expect("just pushed").to_meta();
798 m.version_id = Some(vid);
799 if self.strip_version_ids.load(Ordering::SeqCst) {
800 m.version_id = None;
801 }
802 Ok(m)
803 }
804
805 async fn compose_generation(&self, spec: &ComposeSpec<'_>) -> StoreResult<ObjectMeta> {
806 self.bump("compose_generation");
807 if spec.parts.is_empty() {
808 return Err(StoreError::Other("compose: no parts".into()));
809 }
810 if spec.parts.len() > self.max_parts {
811 return Err(StoreError::Other(format!(
812 "compose: {} parts exceeds the backend maximum {}",
813 spec.parts.len(),
814 self.max_parts
815 )));
816 }
817 let upload_id = self.raw_begin_upload(spec.key);
818 let result = self.compose_inner(spec, &upload_id).await;
822 if result.is_err() && !self.leave_orphan.swap(false, Ordering::SeqCst) {
823 let mut inner = self.inner.lock().unwrap();
824 inner.uploads.remove(&upload_id);
825 }
826 result
827 }
828
829 async fn head(&self, key: &str) -> StoreResult<ObjectMeta> {
830 self.bump("head");
831 if self
832 .fail_head_count
833 .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |c| c.checked_sub(1))
834 .is_ok()
835 {
836 return Err(StoreError::Other("injected head failure".into()));
837 }
838 self.inner
839 .lock()
840 .unwrap()
841 .current(key)
842 .map(|o| o.to_meta())
843 .ok_or_else(|| StoreError::NotFound(key.into()))
844 }
845
846 async fn presign_get(&self, key: &str, ttl_secs: u64) -> StoreResult<String> {
853 self.bump("presign_get");
854 let inner = self.inner.lock().unwrap();
855 if inner.current(key).is_none() {
856 return Err(StoreError::NotFound(key.into()));
857 }
858 Ok(format!("memory://presigned/{key}?ttl={ttl_secs}"))
859 }
860
861 async fn presign_put(&self, key: &str, ttl_secs: u64) -> StoreResult<String> {
866 self.bump("presign_put");
867 Ok(format!("memory://presigned-put/{key}?ttl={ttl_secs}"))
868 }
869
870 async fn get_whole(
871 &self,
872 key: &str,
873 if_match: Option<&str>,
874 ) -> StoreResult<(ObjectMeta, Bytes)> {
875 self.bump("get_whole");
876 let inner = self.inner.lock().unwrap();
877 let o = inner
878 .current(key)
879 .ok_or_else(|| StoreError::NotFound(key.into()))?;
880 if let Some(want) = if_match {
881 if o.etag != want {
882 return Err(StoreError::PreconditionFailed(format!(
883 "get {}: etag {} != {}",
884 key, o.etag, want
885 )));
886 }
887 }
888 Ok((o.to_meta(), o.bytes.clone()))
889 }
890
891 async fn get_range_segments(
898 &self,
899 key: &str,
900 offset: u64,
901 len: u64,
902 if_match: &str,
903 ) -> StoreResult<Vec<Bytes>> {
904 let whole = self.get_range(key, offset, len, if_match).await?;
905 let mut segs: Vec<Bytes> = Vec::new();
909 let (mut at, mut step) = (0usize, 1usize);
910 while at < whole.len() {
911 let n = step.min(whole.len() - at);
912 segs.push(whole.slice(at..at + n)); at += n;
914 step = (step.saturating_mul(3)).min(64 * 1024);
915 }
916 if segs.is_empty() {
917 segs.push(whole);
918 }
919 Ok(segs)
920 }
921
922 async fn get_range(
923 &self,
924 key: &str,
925 offset: u64,
926 len: u64,
927 if_match: &str,
928 ) -> StoreResult<Bytes> {
929 self.bump("get_range");
930 let _in_flight = {
931 let now = self.inflight_get_range.fetch_add(1, Ordering::SeqCst) + 1;
932 self.peak_get_range.fetch_max(now, Ordering::SeqCst);
933 InFlight(&self.inflight_get_range)
934 };
935 if self
937 .fail_get_range_count
938 .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |c| c.checked_sub(1))
939 .is_ok()
940 {
941 return Err(StoreError::Other("injected get_range failure".into()));
942 }
943 let stall = self.stall_next_get_range_ms.swap(0, Ordering::SeqCst);
944 if stall > 0 {
945 tokio::time::sleep(std::time::Duration::from_millis(stall)).await;
946 }
947 let delay = self.get_range_delay_ms.load(Ordering::SeqCst);
948 if delay > 0 {
949 tokio::time::sleep(std::time::Duration::from_millis(delay)).await;
950 }
951 let inner = self.inner.lock().unwrap();
952 let o = inner
953 .current(key)
954 .ok_or_else(|| StoreError::NotFound(key.into()))?;
955 if o.etag != if_match {
956 return Err(StoreError::PreconditionFailed(format!(
957 "get_range {}: etag {} != {}",
958 key, o.etag, if_match
959 )));
960 }
961 let start = offset.min(o.bytes.len() as u64) as usize;
962 let end = (offset + len).min(o.bytes.len() as u64) as usize;
963 Ok(o.bytes.slice(start..end))
964 }
965
966 async fn list(&self, prefix: &str) -> StoreResult<Vec<ListedObject>> {
967 self.bump("list");
968 Ok(self
969 .inner
970 .lock()
971 .unwrap()
972 .chains
973 .range(prefix.to_string()..)
974 .take_while(|(k, _)| k.starts_with(prefix))
975 .filter_map(|(k, c)| {
978 c.current().map(|o| ListedObject {
979 key: k.clone(),
980 size: o.bytes.len() as u64,
981 etag: o.etag.clone(),
982 last_modified_unix: Some(o.last_modified_unix),
983 })
984 })
985 .collect())
986 }
987
988 async fn delete(&self, key: &str) -> StoreResult<()> {
989 self.bump("delete");
990 self.inner.lock().unwrap().push_delete_marker(key);
991 Ok(())
992 }
993
994 async fn delete_if_match(&self, key: &str, etag: &str) -> StoreResult<()> {
995 self.bump("delete");
997 let mut inner = self.inner.lock().unwrap();
998 let refused = match inner.current(key) {
999 None => Some(StoreError::NotFound(key.to_string())),
1000 Some(o) if o.etag != etag => Some(StoreError::PreconditionFailed(format!(
1001 "{key}: If-Match {etag} but the current etag is {}",
1002 o.etag
1003 ))),
1004 Some(_) => None,
1005 };
1006 if let Some(e) = refused {
1007 return Err(e);
1008 }
1009 inner.push_delete_marker(key);
1010 Ok(())
1011 }
1012
1013 async fn head_version(&self, key: &str, version_id: &str) -> StoreResult<ObjectMeta> {
1014 self.bump("head_version");
1015 self.inner
1016 .lock()
1017 .unwrap()
1018 .version(key, version_id)
1019 .map(|o| o.to_meta())
1020 .ok_or_else(|| StoreError::NotFound(format!("{key}?versionId={version_id}")))
1021 }
1022
1023 async fn get_version(&self, key: &str, version_id: &str) -> StoreResult<(ObjectMeta, Bytes)> {
1024 self.bump("get_version");
1025 let inner = self.inner.lock().unwrap();
1026 let o = inner
1027 .version(key, version_id)
1028 .ok_or_else(|| StoreError::NotFound(format!("{key}?versionId={version_id}")))?;
1029 Ok((o.to_meta(), o.bytes.clone()))
1030 }
1031
1032 async fn delete_version(&self, key: &str, version_id: &str) -> StoreResult<()> {
1033 self.bump("delete_version");
1034 self.inner.lock().unwrap().remove_version(key, version_id);
1037 Ok(())
1038 }
1039
1040 async fn list_versions(&self, prefix: &str) -> StoreResult<Vec<ListedVersion>> {
1041 self.bump("list_versions");
1042 let inner = self.inner.lock().unwrap();
1043 let mut out = vec![];
1044 for (k, chain) in inner.chains.range(prefix.to_string()..) {
1045 if !k.starts_with(prefix) {
1046 break;
1047 }
1048 let last = chain.versions.len().saturating_sub(1);
1049 for (i, o) in chain.versions.iter().enumerate() {
1050 out.push(ListedVersion {
1051 key: k.clone(),
1052 version_id: o.version_id.clone(),
1053 etag: o.etag.clone(),
1054 size: o.bytes.len() as u64,
1055 is_current: i == last,
1056 is_delete_marker: o.deleted,
1057 last_modified_unix: Some(o.last_modified_unix),
1058 });
1059 }
1060 }
1061 Ok(out)
1062 }
1063
1064 async fn list_uploads(&self, prefix: &str) -> StoreResult<Vec<PendingUpload>> {
1065 self.bump("list_uploads");
1066 let inner = self.inner.lock().unwrap();
1067 let mut v: Vec<PendingUpload> = inner
1068 .uploads
1069 .iter()
1070 .filter(|(_, m)| m.key.starts_with(prefix))
1071 .map(|(id, m)| PendingUpload {
1072 key: m.key.clone(),
1073 upload_id: id.clone(),
1074 initiated_unix: Some(m.initiated_unix),
1075 })
1076 .collect();
1077 v.sort_by(|a, b| a.upload_id.cmp(&b.upload_id));
1078 Ok(v)
1079 }
1080
1081 async fn abort_upload(&self, _key: &str, upload_id: &str) -> StoreResult<()> {
1082 self.bump("abort_upload");
1083 self.inner.lock().unwrap().uploads.remove(upload_id);
1085 Ok(())
1086 }
1087
1088 async fn bootstrap(&self, _prefix: &str) -> StoreResult<BootstrapReport> {
1089 self.bump("bootstrap");
1090 Ok(BootstrapReport {
1091 notes: vec!["memory store: no bucket posture to verify".into()],
1092 ..Default::default()
1093 })
1094 }
1095
1096 async fn lifecycle_rules(&self) -> StoreResult<Vec<LifecycleView>> {
1097 self.bump("lifecycle_rules");
1098 Ok(self.inner.lock().unwrap().lifecycle.clone())
1099 }
1100
1101 async fn ensure_noncurrent_retention(
1102 &self,
1103 prefix: &str,
1104 days: u64,
1105 ) -> StoreResult<RetentionOutcome> {
1106 self.bump("ensure_noncurrent_retention");
1107 if self.fail_lifecycle_writes.load(Ordering::SeqCst) {
1108 return Err(StoreError::Other("lifecycle writes are denied".into()));
1109 }
1110 let rule_id = format!("flint-lean-noncurrent-{}", prefix.trim_end_matches('/').replace('/', "-"));
1111 let mut inner = self.inner.lock().unwrap();
1112 if let Some(r) = inner
1115 .lifecycle
1116 .iter()
1117 .find(|r| r.enabled && r.prefix == prefix && r.noncurrent_days == Some(days))
1118 {
1119 return Ok(RetentionOutcome {
1120 rule_id: r.id.clone(),
1121 noncurrent_days: days,
1122 created: false,
1123 });
1124 }
1125 inner.lifecycle.retain(|r| r.id != rule_id);
1126 inner.lifecycle.push(LifecycleView {
1127 id: rule_id.clone(),
1128 enabled: true,
1129 prefix: prefix.to_string(),
1130 noncurrent_days: Some(days),
1131 expired_delete_marker: true,
1132 });
1133 Ok(RetentionOutcome { rule_id, noncurrent_days: days, created: true })
1134 }
1135
1136 async fn epoch_read(&self, key: &str) -> StoreResult<Option<EpochState>> {
1137 self.bump("epoch_read");
1138 let inner = self.inner.lock().unwrap();
1139 let Some(o) = inner.current(key) else {
1140 return Ok(None);
1141 };
1142 let body: EpochBody = serde_json::from_slice(&o.bytes)
1143 .map_err(|e| StoreError::Other(format!("epoch body: {}", e)))?;
1144 Ok(Some(EpochState {
1145 holder_id: body.holder_id,
1146 epoch: body.epoch,
1147 token: o.etag.clone(),
1148 last_renew_unix: Some(o.last_modified_unix),
1149 released: body.released,
1150 echo: body.echo,
1151 waiters: body.waiters,
1152 handoff: body.handoff,
1153 }))
1154 }
1155
1156 async fn epoch_acquire(
1157 &self,
1158 key: &str,
1159 holder_id: &str,
1160 supersede: Option<&EpochState>,
1161 ) -> StoreResult<EpochLease> {
1162 self.bump("epoch_acquire");
1163 let epoch = supersede.map_or(1, |s| s.epoch + 1);
1164 let waiters: Vec<String> = supersede
1165 .map(|s| s.waiters.iter().filter(|w| w.as_str() != holder_id).cloned().collect())
1166 .unwrap_or_default();
1167 let condition = match supersede {
1168 None => PutCondition::IfNoneMatchAny,
1169 Some(s) => PutCondition::IfMatch(s.token.clone()),
1170 };
1171 let token = self.epoch_store(
1172 key,
1173 EpochBody {
1174 holder_id: holder_id.into(),
1175 epoch,
1176 released: false,
1177 echo: None,
1178 waiters: waiters.clone(),
1179 handoff: None,
1180 },
1181 &condition,
1182 )?;
1183 Ok(EpochLease { holder_id: holder_id.into(), epoch, token, waiters })
1184 }
1185
1186 async fn epoch_renew(
1187 &self,
1188 key: &str,
1189 lease: &EpochLease,
1190 echo: Option<&str>,
1191 ) -> StoreResult<EpochLease> {
1192 self.bump("epoch_renew");
1193 if self
1194 .fail_epoch_renew_count
1195 .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |c| c.checked_sub(1))
1196 .is_ok()
1197 {
1198 return Err(StoreError::Other("injected renew failure: store unreachable".into()));
1199 }
1200 let token = self.epoch_store(
1201 key,
1202 EpochBody {
1203 holder_id: lease.holder_id.clone(),
1204 epoch: lease.epoch,
1205 released: false,
1206 echo: echo.map(|e| e.to_string()),
1207 waiters: lease.waiters.clone(),
1208 handoff: None,
1209 },
1210 &PutCondition::IfMatch(lease.token.clone()),
1211 )?;
1212 Ok(EpochLease {
1213 holder_id: lease.holder_id.clone(),
1214 epoch: lease.epoch,
1215 token,
1216 waiters: lease.waiters.clone(),
1217 })
1218 }
1219
1220 async fn epoch_release(&self, key: &str, lease: &EpochLease) -> StoreResult<()> {
1221 self.bump("epoch_release");
1222 self.epoch_store(
1227 key,
1228 EpochBody {
1229 holder_id: lease.holder_id.clone(),
1230 epoch: lease.epoch,
1231 released: true,
1232 echo: None,
1233 waiters: lease.waiters.clone(),
1234 handoff: None,
1235 },
1236 &PutCondition::IfMatch(lease.token.clone()),
1237 )?;
1238 Ok(())
1239 }
1240
1241 async fn epoch_handoff(
1242 &self,
1243 key: &str,
1244 lease: &EpochLease,
1245 echo: Option<&str>,
1246 ) -> StoreResult<()> {
1247 self.bump("epoch_handoff");
1248 let mut rest = lease.waiters.clone();
1249 let head = if rest.is_empty() { None } else { Some(rest.remove(0)) };
1250 self.epoch_store(
1251 key,
1252 EpochBody {
1253 holder_id: lease.holder_id.clone(),
1254 epoch: lease.epoch,
1255 released: true,
1256 echo: echo.map(|e| e.to_string()),
1257 waiters: rest,
1258 handoff: head,
1259 },
1260 &PutCondition::IfMatch(lease.token.clone()),
1261 )?;
1262 Ok(())
1263 }
1264
1265 async fn epoch_enqueue(
1266 &self,
1267 key: &str,
1268 observed: &EpochState,
1269 holder_id: &str,
1270 ) -> StoreResult<EpochState> {
1271 self.bump("epoch_enqueue");
1272 let mut waiters = observed.waiters.clone();
1273 if !waiters.iter().any(|w| w == holder_id) {
1274 waiters.push(holder_id.to_string());
1275 }
1276 let token = self.epoch_store(
1277 key,
1278 EpochBody {
1279 holder_id: observed.holder_id.clone(),
1280 epoch: observed.epoch,
1281 released: observed.released,
1282 echo: observed.echo.clone(),
1283 waiters: waiters.clone(),
1284 handoff: observed.handoff.clone(),
1285 },
1286 &PutCondition::IfMatch(observed.token.clone()),
1287 )?;
1288 Ok(EpochState {
1289 holder_id: observed.holder_id.clone(),
1290 epoch: observed.epoch,
1291 token,
1292 last_renew_unix: Some(now_unix()),
1293 echo: observed.echo.clone(),
1294 released: observed.released,
1295 waiters,
1296 handoff: observed.handoff.clone(),
1297 })
1298 }
1299
1300 fn min_part_size(&self) -> u64 {
1301 self.min_part
1302 }
1303
1304 fn max_parts(&self) -> usize {
1305 self.max_parts
1306 }
1307
1308 fn upload_gate(&self) -> Option<std::sync::Arc<crate::gate::ByteGate>> {
1309 self.upload_gate.clone()
1310 }
1311}
1312
1313impl MemoryStore {
1314 async fn stage_one_part(
1320 &self,
1321 spec: &ComposeSpec<'_>,
1322 upload_id: &str,
1323 i: usize,
1324 p: &PartSource,
1325 ) -> StoreResult<()> {
1326 match p {
1327 PartSource::Local { offset, len } => {
1328 let _permit = match &self.upload_gate {
1337 Some(g) => Some(g.acquire(*len).await),
1338 None => None,
1339 };
1340 let path = spec.local_path.to_path_buf();
1341 let (offset, len) = (*offset, *len);
1342 let bytes = tokio::task::spawn_blocking(move || read_local(&path, offset, len))
1343 .await
1344 .map_err(|e| StoreError::Other(format!("local read join: {e}")))??;
1345 self.land_part(spec, upload_id, i, len, bytes)
1346 }
1347 PartSource::BaseCopy { offset, len } => {
1348 let want = spec.base_etag.as_deref().ok_or_else(|| {
1349 StoreError::Other("compose: BaseCopy without base_etag".into())
1350 })?;
1351 let bytes = {
1352 let inner = self.inner.lock().unwrap();
1353 let base_key = spec.base_key.unwrap_or(spec.key);
1354 let base = inner.current(base_key).ok_or_else(|| {
1355 StoreError::PreconditionFailed("copy-source: no base object".into())
1356 })?;
1357 if base.etag != want {
1358 return Err(StoreError::PreconditionFailed(
1359 "copy-source-if-match: base etag differs".into(),
1360 ));
1361 }
1362 let s = *offset as usize;
1363 let e = (*offset + *len) as usize;
1364 if e > base.bytes.len() {
1365 return Err(StoreError::Other(format!(
1366 "copy-source range [{}, {}) beyond base size {}",
1367 s,
1368 e,
1369 base.bytes.len()
1370 )));
1371 }
1372 base.bytes.slice(s..e)
1373 };
1374 self.land_part(spec, upload_id, i, *len, bytes)
1375 }
1376 }
1377 }
1378
1379 fn land_part(
1382 &self,
1383 spec: &ComposeSpec<'_>,
1384 upload_id: &str,
1385 i: usize,
1386 want_len: u64,
1387 bytes: Bytes,
1388 ) -> StoreResult<()> {
1389 if bytes.len() as u64 != want_len {
1390 return Err(StoreError::Other(format!("compose: part {} short read", i)));
1391 }
1392 spec.note_progress(bytes.len() as u64);
1393 self.inner
1394 .lock()
1395 .unwrap()
1396 .uploads
1397 .get_mut(upload_id)
1398 .ok_or_else(|| StoreError::NoSuchUpload(upload_id.into()))?
1399 .parts
1400 .insert(i, bytes);
1401 Ok(())
1402 }
1403
1404 async fn compose_inner(
1405 &self,
1406 spec: &ComposeSpec<'_>,
1407 upload_id: &str,
1408 ) -> StoreResult<ObjectMeta> {
1409 self.bump("compose_inner");
1410 if spec.crc64.is_none()
1413 && spec.parts.iter().any(|p| matches!(p, PartSource::BaseCopy { .. }))
1414 {
1415 return Err(StoreError::Other(
1416 "compose: the checksum cannot be accumulated over a copied part; supply crc64".into(),
1417 ));
1418 }
1419 let mut expect = 0u64;
1423 for (i, p) in spec.parts.iter().enumerate() {
1424 let (off, len) = match p {
1425 PartSource::Local { offset, len } | PartSource::BaseCopy { offset, len } => {
1426 (*offset, *len)
1427 }
1428 };
1429 if off != expect {
1430 return Err(StoreError::Other(format!(
1431 "compose: part {} at offset {} but object is contiguous to {}",
1432 i, off, expect
1433 )));
1434 }
1435 if len < self.min_part && i + 1 != spec.parts.len() {
1436 return Err(StoreError::Other(format!(
1437 "compose: part {} len {} under the backend minimum {}",
1438 i, len, self.min_part
1439 )));
1440 }
1441 expect = off + len;
1442 }
1443
1444 {
1451 use futures::stream::StreamExt;
1452 let par = self.part_parallelism.max(1);
1453 let mut futs = Vec::with_capacity(spec.parts.len());
1454 for (i, p) in spec.parts.iter().enumerate() {
1455 futs.push(self.stage_one_part(spec, upload_id, i, p));
1456 }
1457 let staged: Vec<StoreResult<()>> =
1458 futures::stream::iter(futs).buffer_unordered(par).collect().await;
1459 staged.into_iter().collect::<StoreResult<Vec<()>>>()?;
1460 }
1461 let mut assembled = Vec::with_capacity(expect as usize);
1463 {
1464 let inner = self.inner.lock().unwrap();
1465 let up = inner
1466 .uploads
1467 .get(upload_id)
1468 .ok_or_else(|| StoreError::NoSuchUpload(upload_id.into()))?;
1469 for i in 0..spec.parts.len() {
1470 let bytes = up.parts.get(&i).ok_or_else(|| {
1471 StoreError::Other(format!("compose: part {} was staged but did not land", i))
1472 })?;
1473 assembled.extend_from_slice(bytes);
1474 }
1475 }
1476
1477 let actual = crc64_nvme(&assembled);
1487 if let Some(claimed) = spec.crc64 {
1488 if actual != claimed {
1489 return Err(StoreError::ChecksumMismatch(format!(
1490 "compose {}: claimed {:#x}, content is {:#x}",
1491 spec.key, claimed, actual
1492 )));
1493 }
1494 }
1495
1496 match self.inject.swap(INJECT_NONE, Ordering::SeqCst) {
1497 INJECT_CRASH_BEFORE_COMPLETE => {
1498 self.leave_orphan.store(true, Ordering::SeqCst);
1502 return Err(StoreError::Other("injected: crash before Complete".into()));
1503 }
1504 INJECT_TORN_COMPLETE => {
1505 self.finish_complete(spec, upload_id, assembled)?;
1507 return Err(StoreError::Other("injected: Complete response lost".into()));
1508 }
1509 _ => {}
1510 }
1511
1512 let meta = self.finish_complete(spec, upload_id, assembled)?;
1513 Ok(meta)
1514 }
1515
1516 fn finish_complete(
1517 &self,
1518 spec: &ComposeSpec<'_>,
1519 upload_id: &str,
1520 assembled: Vec<u8>,
1521 ) -> StoreResult<ObjectMeta> {
1522 let actual = crc64_nvme(&assembled);
1523 if let Some(claimed) = spec.crc64 {
1524 if actual != claimed {
1525 return Err(StoreError::ChecksumMismatch(format!(
1526 "compose {}: claimed {:#x}, assembled is {:#x}",
1527 spec.key, claimed, actual
1528 )));
1529 }
1530 }
1531 let mut inner = self.inner.lock().unwrap();
1532 if inner.uploads.remove(upload_id).is_none() {
1533 return Err(StoreError::NoSuchUpload(upload_id.into()));
1535 }
1536 self.check_condition(inner.current(spec.key), &spec.condition)?;
1537 let parts = spec.parts.len();
1538 let bytes = Bytes::from(assembled);
1539 let obj = StoredObject {
1540 etag: mpu_etag(&bytes, parts),
1541 crc64: actual,
1542 meta: spec.stamps.to_meta().into_iter().collect(),
1543 last_modified_unix: now_unix(),
1544 bytes,
1545 version_id: String::new(),
1546 deleted: false,
1547 };
1548 let vid = inner.push(spec.key, obj);
1549 let mut m = inner.current(spec.key).expect("just pushed").to_meta();
1550 m.version_id = Some(vid);
1551 Ok(m)
1552 }
1553}
1554
1555fn read_local(path: &std::path::Path, offset: u64, len: u64) -> StoreResult<Bytes> {
1556 use std::io::{Read, Seek, SeekFrom};
1557 let mut f = std::fs::File::open(path)
1558 .map_err(|e| StoreError::Other(format!("local open {:?}: {}", path, e)))?;
1559 f.seek(SeekFrom::Start(offset))
1560 .map_err(|e| StoreError::Other(format!("local seek: {}", e)))?;
1561 let mut buf = vec![0u8; len as usize];
1562 f.read_exact(&mut buf)
1563 .map_err(|e| StoreError::Other(format!("local read: {}", e)))?;
1564 Ok(Bytes::from(buf))
1565}
1566
1567#[cfg(test)]
1568mod tests {
1569 use super::*;
1570
1571 fn stamps(generation: u64) -> GenerationStamps {
1572 GenerationStamps { generation, epoch: 1, flush_uuid: format!("u-{}", generation), boundary_source: None, posix: None }
1573 }
1574
1575 #[tokio::test]
1576 async fn conditional_puts_and_gets_enforce_the_contract() {
1577 let s = MemoryStore::new();
1578 let body = Bytes::from_static(b"gen-1");
1579 let crc = crc64_nvme(&body);
1580
1581 let err = s
1583 .put_whole("k", body.clone(), &PutCondition::IfNoneMatchAny, &stamps(1), crc ^ 1)
1584 .await
1585 .unwrap_err();
1586 assert!(matches!(err, StoreError::ChecksumMismatch(_)));
1587
1588 let m1 = s
1589 .put_whole("k", body.clone(), &PutCondition::IfNoneMatchAny, &stamps(1), crc)
1590 .await
1591 .unwrap();
1592 let err = s
1594 .put_whole("k", body.clone(), &PutCondition::IfNoneMatchAny, &stamps(1), crc)
1595 .await
1596 .unwrap_err();
1597 assert!(matches!(err, StoreError::PreconditionFailed(_)));
1598
1599 let b2 = Bytes::from_static(b"gen-2");
1601 let m2 = s
1602 .put_whole("k", b2.clone(), &PutCondition::IfMatch(m1.etag.clone()), &stamps(2), crc64_nvme(&b2))
1603 .await
1604 .unwrap();
1605 let err = s
1606 .put_whole("k", b2.clone(), &PutCondition::IfMatch(m1.etag.clone()), &stamps(3), crc64_nvme(&b2))
1607 .await
1608 .unwrap_err();
1609 assert!(matches!(err, StoreError::PreconditionFailed(_)));
1610
1611 let (gm, gb) = s.get_whole("k", Some(&m2.etag)).await.unwrap();
1613 assert_eq!(gb, b2);
1614 assert_eq!(GenerationStamps::from_meta(&gm.meta).unwrap().generation, 2);
1615 let err = s.get_whole("k", Some(&m1.etag)).await.unwrap_err();
1616 assert!(matches!(err, StoreError::PreconditionFailed(_)));
1617 }
1618
1619 #[tokio::test]
1620 async fn epoch_cas_lease_lifecycle() {
1621 let s = MemoryStore::new();
1622 const K: &str = "vol/.flint-epoch";
1623
1624 let l1 = s.epoch_acquire(K, "hub-a", None).await.unwrap();
1626 assert_eq!(l1.epoch, 1);
1627 let err = s.epoch_acquire(K, "hub-b", None).await.unwrap_err();
1628 assert!(matches!(err, StoreError::PreconditionFailed(_)));
1629
1630 let l1b = s.epoch_renew(K, &l1, None).await.unwrap();
1633 assert_ne!(l1b.token, l1.token, "renew must rotate the CAS token");
1634 let err = s.epoch_renew(K, &l1, None).await.unwrap_err();
1635 assert!(matches!(err, StoreError::PreconditionFailed(_)), "stale token must be dead");
1636
1637 let observed = s.epoch_read(K).await.unwrap().unwrap();
1641 assert_eq!(observed.holder_id, "hub-a");
1642 let l2 = s.epoch_acquire(K, "hub-b", Some(&observed)).await.unwrap();
1643 assert_eq!(l2.epoch, 2);
1644 let err = s.epoch_renew(K, &l1b, None).await.unwrap_err();
1645 assert!(matches!(err, StoreError::PreconditionFailed(_)), "deposed renew must fail");
1646 let err = s.epoch_release(K, &l1b).await.unwrap_err();
1647 assert!(matches!(err, StoreError::PreconditionFailed(_)), "deposed release must fail");
1648
1649 s.epoch_release(K, &l2).await.unwrap();
1653 let after = s.epoch_read(K).await.unwrap().expect("the cell must survive a release");
1654 assert!(after.released, "a clean shutdown marks the cell released");
1655 assert_eq!(after.epoch, 2, "and the epoch number is preserved");
1656 assert_eq!(after.holder_id, "hub-b");
1657
1658 let l3 = s.epoch_acquire(K, "hub-c", Some(&after)).await.unwrap();
1661 assert_eq!(l3.epoch, 3, "numbering continues, it does not reset");
1662 assert!(
1663 !s.epoch_read(K).await.unwrap().unwrap().released,
1664 "a fresh claim clears the released mark"
1665 );
1666 }
1667
1668 #[tokio::test]
1676 async fn epoch_queue_hands_the_cell_to_the_head_and_keeps_the_echo() {
1677 const K: &str = "t/.flint/lean/epoch";
1678 let s = MemoryStore::new();
1679 let a = s.epoch_acquire(K, "a", None).await.unwrap();
1680 let seen = s.epoch_read(K).await.unwrap().unwrap();
1681 let seen = s.epoch_enqueue(K, &seen, "b").await.unwrap();
1682 let seen = s.epoch_enqueue(K, &seen, "c").await.unwrap();
1683 let seen = s.epoch_enqueue(K, &seen, "b").await.unwrap();
1684 assert_eq!(seen.waiters, vec!["b", "c"], "a waiter is appended once, in order");
1685 let err = s.epoch_renew(K, &a, None).await.unwrap_err();
1688 assert!(matches!(err, StoreError::PreconditionFailed(_)));
1689 let adopted = EpochLease {
1692 holder_id: "a".into(),
1693 epoch: seen.epoch,
1694 token: seen.token.clone(),
1695 waiters: seen.waiters.clone(),
1696 };
1697 let a = s.epoch_renew(K, &adopted, Some("echo-a")).await.unwrap();
1698 assert_eq!(s.epoch_read(K).await.unwrap().unwrap().waiters, vec!["b", "c"]);
1699 s.epoch_handoff(K, &a, Some("echo-a")).await.unwrap();
1701 let cell = s.epoch_read(K).await.unwrap().unwrap();
1702 assert!(cell.released);
1703 assert_eq!(cell.handoff.as_deref(), Some("b"));
1704 assert_eq!(cell.waiters, vec!["c"]);
1705 assert_eq!(cell.echo.as_deref(), Some("echo-a"), "the last barrier's echo survives the handoff");
1706 let b = s.epoch_acquire(K, "b", Some(&cell)).await.unwrap();
1708 assert_eq!(b.epoch, cell.epoch + 1);
1709 assert_eq!(b.waiters, vec!["c"]);
1710 let cell = s.epoch_read(K).await.unwrap().unwrap();
1711 assert!(!cell.released && cell.handoff.is_none());
1712 assert_eq!(cell.holder_id, "b");
1713 assert_eq!(cell.waiters, vec!["c"]);
1714 s.epoch_handoff(K, &b, None).await.unwrap();
1717 let cell = s.epoch_read(K).await.unwrap().unwrap();
1718 assert_eq!(cell.handoff.as_deref(), Some("c"));
1719 assert!(cell.waiters.is_empty());
1720 let c = s.epoch_acquire(K, "c", Some(&cell)).await.unwrap();
1721 assert!(c.waiters.is_empty());
1722 s.epoch_handoff(K, &c, None).await.unwrap();
1723 let cell = s.epoch_read(K).await.unwrap().unwrap();
1724 assert!(cell.released && cell.handoff.is_none() && cell.waiters.is_empty());
1725 }
1726
1727 #[tokio::test]
1728 async fn compose_mixes_local_and_guarded_base_copy() {
1729 let s = MemoryStore::new();
1730 let base_body = Bytes::from_static(b"AAAABBBB");
1732 let base = s
1733 .put_whole("f", base_body.clone(), &PutCondition::IfNoneMatchAny, &stamps(1), crc64_nvme(&base_body))
1734 .await
1735 .unwrap();
1736 let dir = tempfile::TempDir::new().unwrap();
1738 let local = dir.path().join("f.local");
1739 std::fs::write(&local, b"XXXXBBBB").unwrap();
1740
1741 let spec = ComposeSpec {
1742 progress: None,
1743 key: "f",
1744 local_path: &local,
1745 parts: vec![
1746 PartSource::Local { offset: 0, len: 4 },
1747 PartSource::BaseCopy { offset: 4, len: 4 },
1748 ],
1749 base_key: None,
1750 base_etag: Some(base.etag.clone()),
1751 condition: PutCondition::IfMatch(base.etag.clone()),
1752 stamps: stamps(2),
1753 crc64: Some(crc64_nvme(b"XXXXBBBB")),
1754 };
1755 let m2 = s.compose_generation(&spec).await.unwrap();
1756 let (_, bytes) = s.get_whole("f", Some(&m2.etag)).await.unwrap();
1757 assert_eq!(bytes, Bytes::from_static(b"XXXXBBBB"));
1758 assert!(m2.etag.ends_with("-2\""), "multipart etag must not look like a content hash");
1759
1760 s.raw_put("f", Bytes::from_static(b"foreignXX"), vec![]);
1763 let err = s.compose_generation(&spec).await.unwrap_err();
1764 assert!(matches!(err, StoreError::PreconditionFailed(_)));
1765 assert!(
1766 s.list_uploads("f").await.unwrap().is_empty(),
1767 "a failed compose must abort its MPU (A9)"
1768 );
1769 }
1770 #[tokio::test]
1775 async fn compose_without_a_claimed_checksum_publishes_the_content_crc() {
1776 let s = MemoryStore::new();
1777 let dir = tempfile::TempDir::new().unwrap();
1778 let local = dir.path().join("g.local");
1779 let content: Vec<u8> = (0..24u8).collect();
1780 std::fs::write(&local, &content).unwrap();
1781 let spec = ComposeSpec {
1782 progress: None,
1783 key: "g",
1784 local_path: &local,
1785 parts: vec![
1786 PartSource::Local { offset: 0, len: 8 },
1787 PartSource::Local { offset: 8, len: 8 },
1788 PartSource::Local { offset: 16, len: 8 },
1789 ],
1790 base_key: None,
1791 base_etag: None,
1792 condition: PutCondition::IfNoneMatchAny,
1793 stamps: stamps(1),
1794 crc64: None,
1795 };
1796 let m = s.compose_generation(&spec).await.unwrap();
1797 assert_eq!(m.crc64_b64.as_deref(), Some(crc64_to_b64(crc64_nvme(&content)).as_str()));
1798 let (h, bytes) = s.get_whole("g", None).await.unwrap();
1799 assert_eq!(&bytes[..], &content[..]);
1800 assert_eq!(h.crc64_b64, m.crc64_b64);
1801
1802 let base_body = Bytes::from_static(b"AAAABBBB");
1803 let base = s
1804 .put_whole("h", base_body.clone(), &PutCondition::IfNoneMatchAny, &stamps(1), crc64_nvme(&base_body))
1805 .await
1806 .unwrap();
1807 std::fs::write(&local, b"XXXXBBBB").unwrap();
1808 let err = s
1809 .compose_generation(&ComposeSpec {
1810 progress: None,
1811 key: "h",
1812 local_path: &local,
1813 parts: vec![
1814 PartSource::Local { offset: 0, len: 4 },
1815 PartSource::BaseCopy { offset: 4, len: 4 },
1816 ],
1817 base_key: None,
1818 base_etag: Some(base.etag.clone()),
1819 condition: PutCondition::IfMatch(base.etag.clone()),
1820 stamps: stamps(2),
1821 crc64: None,
1822 })
1823 .await
1824 .unwrap_err();
1825 assert!(matches!(err, StoreError::Other(ref m) if m.contains("copied part")), "{err:?}");
1826 assert!(s.list_uploads("h").await.unwrap().is_empty(), "nothing pending after a refusal");
1827 }
1828}
1829
1830#[cfg(test)]
1831mod copy_tests {
1832 use super::*;
1833 use crate::{crc64_from_b64, crc64_to_b64};
1834
1835 fn stamps(g: u64) -> GenerationStamps {
1836 GenerationStamps {
1837 generation: g,
1838 epoch: 7,
1839 flush_uuid: format!("flush-{g}"),
1840 boundary_source: None,
1841 posix: None,
1842 }
1843 }
1844
1845 #[tokio::test]
1846 async fn a_copy_carries_the_bytes_and_the_checksum_but_not_the_stamps() {
1847 let s = MemoryStore::new();
1848 let body = Bytes::from_static(b"the bytes that move");
1849 let src = s
1850 .put_whole("a.txt", body.clone(), &PutCondition::IfNoneMatchAny, &stamps(3), crc64_nvme(&body))
1851 .await
1852 .unwrap();
1853
1854 let dst = s
1855 .copy_object("a.txt", Some(&src.etag), "b.txt", &PutCondition::IfNoneMatchAny, &stamps(1))
1856 .await
1857 .unwrap();
1858
1859 let (meta, got) = s.get_whole("b.txt", None).await.unwrap();
1860 assert_eq!(got, body, "a copy is byte-identical or it is not a copy");
1861 assert_eq!(
1862 dst.crc64_b64, src.crc64_b64,
1863 "identical bytes have an identical checksum — a copy cannot change one"
1864 );
1865 assert_eq!(
1868 meta.meta.get("flint-gen").map(String::as_str),
1869 Some("1"),
1870 "the destination's generation is its OWN, not the source's 3: {:?}",
1871 meta.meta
1872 );
1873 assert_eq!(
1874 meta.meta.get(GenerationStamps::META_FLUSH_UUID).map(String::as_str),
1875 Some("flush-1"),
1876 "and its flush_uuid names the publish that WROTE IT, not flush-3: {:?}",
1877 meta.meta
1878 );
1879 assert!(s.head("a.txt").await.is_ok());
1881 }
1882
1883 #[tokio::test]
1884 async fn a_copy_refuses_when_the_source_moved_under_it() {
1885 let s = MemoryStore::new();
1886 let v1 = Bytes::from_static(b"version one");
1887 let src = s
1888 .put_whole("a.txt", v1, &PutCondition::IfNoneMatchAny, &stamps(1), crc64_nvme(b"version one"))
1889 .await
1890 .unwrap();
1891 let v2 = Bytes::from_static(b"version two");
1892 s.put_whole("a.txt", v2, &PutCondition::IfMatch(src.etag.clone()), &stamps(2), crc64_nvme(b"version two"))
1893 .await
1894 .unwrap();
1895
1896 let err = s
1897 .copy_object("a.txt", Some(&src.etag), "b.txt", &PutCondition::IfNoneMatchAny, &stamps(1))
1898 .await
1899 .expect_err("copying an object that moved copies something the caller never read");
1900 assert!(matches!(err, StoreError::PreconditionFailed(_)), "{err:?}");
1901 assert!(matches!(s.head("b.txt").await, Err(StoreError::NotFound(_))), "and nothing landed");
1902 }
1903
1904 #[tokio::test]
1905 async fn a_copy_honours_the_destination_precondition() {
1906 let s = MemoryStore::new();
1907 let a = Bytes::from_static(b"aaaa");
1908 s.put_whole("a.txt", a, &PutCondition::IfNoneMatchAny, &stamps(1), crc64_nvme(b"aaaa"))
1909 .await
1910 .unwrap();
1911 let b = Bytes::from_static(b"bbbb");
1912 let existing = s
1913 .put_whole("b.txt", b.clone(), &PutCondition::IfNoneMatchAny, &stamps(1), crc64_nvme(b"bbbb"))
1914 .await
1915 .unwrap();
1916
1917 let err = s
1918 .copy_object("a.txt", None, "b.txt", &PutCondition::IfNoneMatchAny, &stamps(2))
1919 .await
1920 .expect_err("a rename must not silently clobber its destination");
1921 assert!(matches!(err, StoreError::PreconditionFailed(_)), "{err:?}");
1922 let (_, still) = s.get_whole("b.txt", Some(&existing.etag)).await.unwrap();
1923 assert_eq!(still, b, "the destination is untouched by a refused copy");
1924 }
1925
1926 #[tokio::test]
1931 async fn a_compose_can_copy_ranges_from_a_different_key() {
1932 let s = MemoryStore::new();
1933 let body = Bytes::from_static(b"AAAABBBB");
1934 let src = s
1935 .put_whole("old/name", body.clone(), &PutCondition::IfNoneMatchAny, &stamps(1), crc64_nvme(&body))
1936 .await
1937 .unwrap();
1938
1939 let dir = tempfile::TempDir::new().unwrap();
1940 let spec = ComposeSpec {
1941 progress: None,
1942 key: "new/name",
1943 local_path: &dir.path().join("nothing-here"),
1945 parts: vec![PartSource::BaseCopy { offset: 0, len: 8 }],
1946 base_key: Some("old/name"),
1947 base_etag: Some(src.etag.clone()),
1948 condition: PutCondition::IfNoneMatchAny,
1949 stamps: stamps(1),
1950 crc64: Some(crc64_nvme(&body)),
1951 };
1952 let m = s.compose_generation(&spec).await.unwrap();
1953 let (_, got) = s.get_whole("new/name", Some(&m.etag)).await.unwrap();
1954 assert_eq!(got, body, "the ranges came from the OTHER key");
1955 assert!(s.head("old/name").await.is_ok(), "and the source survives");
1956 }
1957
1958 #[test]
1959 fn crc64_b64_round_trips_and_refuses_what_is_not_one() {
1960 for v in [0u64, 1, u64::MAX, 0x0123_4567_89ab_cdef] {
1961 assert_eq!(crc64_from_b64(&crc64_to_b64(v)), Some(v), "round trip {v:#x}");
1962 }
1963 for bad in ["", "AAAA", "not-base64!!", "AAAAAAAAAAAA", "AAAAAAAAAAA", "AAAAAAAAAAAAAAA="] {
1972 assert_eq!(crc64_from_b64(bad), None, "{bad:?} is not a canonical CRC-64");
1973 }
1974 assert_eq!(crc64_from_b64("AAAAAAAAAAA="), Some(0), "the canonical form IS accepted");
1975 }
1976}