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 request_delay_ms: AtomicU64,
202 inflight_requests: AtomicU64,
205 peak_requests: AtomicU64,
206 deny_list_matching: Mutex<Option<String>>,
209 hold_puts: AtomicBool,
214 hold_puts_matching: Mutex<Option<String>>,
217 held_puts: Mutex<Vec<tokio::sync::oneshot::Sender<()>>>,
218 inflight_get_range: AtomicU64,
221 peak_get_range: AtomicU64,
222 strip_version_ids: AtomicBool,
227 fail_lifecycle_writes: AtomicBool,
231 if_match_missing_412: AtomicBool,
238 ops: Mutex<std::collections::BTreeMap<&'static str, u64>>,
242 pub min_part: u64,
243 pub max_parts: usize,
244 part_parallelism: usize,
248 upload_gate: Option<std::sync::Arc<crate::gate::ByteGate>>,
252}
253
254struct InFlight<'a>(&'a AtomicU64);
257impl Drop for InFlight<'_> {
258 fn drop(&mut self) {
259 self.0.fetch_sub(1, Ordering::SeqCst);
260 }
261}
262
263impl Default for MemoryStore {
264 fn default() -> Self {
265 Self::new()
266 }
267}
268
269impl MemoryStore {
270 pub fn backdate_epoch(&self, key: &str, secs: u64) {
274 let mut inner = self.inner.lock().unwrap();
275 if let Some(chain) = inner.chains.get_mut(key) {
276 for v in chain.versions.iter_mut() {
277 v.last_modified_unix = v.last_modified_unix.saturating_sub(secs);
278 }
279 }
280 }
281
282 pub fn backdate_upload(&self, upload_id: &str, secs: u64) {
285 if let Some(m) = self.inner.lock().unwrap().uploads.get_mut(upload_id) {
286 m.initiated_unix = m.initiated_unix.saturating_sub(secs);
287 }
288 }
289
290 pub fn plant_lifecycle_rule(&self, rule: LifecycleView) {
293 self.inner.lock().unwrap().lifecycle.push(rule);
294 }
295
296 pub fn new() -> Self {
297 MemoryStore {
298 ops: Mutex::new(Default::default()),
299 inner: Mutex::new(Inner::default()),
300 upload_seq: AtomicU64::new(1),
301 inject: AtomicU8::new(INJECT_NONE),
302 leave_orphan: AtomicBool::new(false),
303 fail_get_range_count: AtomicU64::new(0),
304 fail_head_count: AtomicU64::new(0),
305 fail_epoch_renew_count: AtomicU64::new(0),
306 fail_put_after_land_count: AtomicU64::new(0),
307 fail_put_after_land_key: Mutex::new(String::new()),
308 stall_next_get_range_ms: AtomicU64::new(0),
309 get_range_delay_ms: AtomicU64::new(0),
310 put_whole_delay_ms: AtomicU64::new(0),
311 request_delay_ms: AtomicU64::new(0),
312 inflight_requests: AtomicU64::new(0),
313 peak_requests: AtomicU64::new(0),
314 deny_list_matching: Mutex::new(None),
315 hold_puts: AtomicBool::new(false),
316 hold_puts_matching: Mutex::new(None),
317 held_puts: Mutex::new(Vec::new()),
318 inflight_get_range: AtomicU64::new(0),
319 peak_get_range: AtomicU64::new(0),
320 strip_version_ids: AtomicBool::new(false),
321 fail_lifecycle_writes: AtomicBool::new(false),
322 if_match_missing_412: AtomicBool::new(false),
323 min_part: 1,
326 max_parts: 10_000,
327 part_parallelism: 1,
328 upload_gate: None,
329 }
330 }
331
332 pub fn with_if_match_missing_as_412(self) -> Self {
335 self.if_match_missing_412.store(true, Ordering::SeqCst);
336 self
337 }
338
339 pub fn with_part_parallelism(mut self, n: usize) -> Self {
342 self.part_parallelism = n.max(1);
343 self
344 }
345
346 pub fn with_upload_inflight_max_bytes(mut self, n: u64) -> Self {
349 self.upload_gate = (n > 0).then(|| std::sync::Arc::new(crate::gate::ByteGate::new(n)));
350 self
351 }
352
353 pub fn inject_put_whole_delay_ms(&self, ms: u64) {
359 self.put_whole_delay_ms.store(ms, Ordering::SeqCst);
360 }
361
362 pub fn inject_request_delay_ms(&self, ms: u64) {
365 self.request_delay_ms.store(ms, Ordering::SeqCst);
366 }
367
368 pub fn peak_requests_in_flight(&self) -> u64 {
372 self.peak_requests.load(Ordering::SeqCst)
373 }
374
375 pub fn reset_peak_requests_in_flight(&self) {
376 self.peak_requests.store(0, Ordering::SeqCst);
377 }
378
379 pub fn deny_list_matching(&self, m: Option<&str>) {
382 *self.deny_list_matching.lock().unwrap() = m.map(str::to_string);
383 }
384
385 async fn pace(&self) {
387 let ms = self.request_delay_ms.load(Ordering::SeqCst);
388 if ms == 0 {
389 return;
390 }
391 let now = self.inflight_requests.fetch_add(1, Ordering::SeqCst) + 1;
392 self.peak_requests.fetch_max(now, Ordering::SeqCst);
393 let _left = InFlight(&self.inflight_requests);
394 tokio::time::sleep(std::time::Duration::from_millis(ms)).await;
395 }
396
397 pub fn strip_version_ids(&self, on: bool) {
400 self.strip_version_ids.store(on, Ordering::SeqCst);
401 }
402
403 pub fn fail_lifecycle_writes(&self, on: bool) {
405 self.fail_lifecycle_writes.store(on, Ordering::SeqCst);
406 }
407
408 pub fn inject_torn_complete(&self) {
410 self.inject.store(INJECT_TORN_COMPLETE, Ordering::SeqCst);
411 }
412
413 pub fn inject_crash_before_complete(&self) {
415 self.inject.store(INJECT_CRASH_BEFORE_COMPLETE, Ordering::SeqCst);
416 }
417
418 pub fn inject_compose_swept(&self) {
421 self.inject.store(INJECT_SWEPT, Ordering::SeqCst);
422 }
423
424 pub fn inject_get_range_failure(&self) {
427 self.inject_get_range_failures(1);
428 }
429
430 pub fn inject_get_range_failures(&self, n: u64) {
433 self.fail_get_range_count.store(n, Ordering::SeqCst);
434 }
435
436 pub fn inject_get_range_delay_ms(&self, ms: u64) {
442 self.get_range_delay_ms.store(ms, Ordering::SeqCst);
443 }
444
445 pub fn inject_put_hold(&self, on: bool) {
451 *self.hold_puts_matching.lock().unwrap() = None;
452 self.hold_puts.store(on, Ordering::SeqCst);
453 }
454
455 pub fn inject_put_hold_matching(&self, key_contains: &str) {
460 *self.hold_puts_matching.lock().unwrap() = Some(key_contains.to_string());
461 self.hold_puts.store(true, Ordering::SeqCst);
462 }
463
464 pub fn held_puts(&self) -> usize {
466 self.held_puts.lock().unwrap().len()
467 }
468
469 pub fn release_held_puts(&self) {
471 for tx in self.held_puts.lock().unwrap().drain(..) {
472 let _ = tx.send(());
473 }
474 }
475
476 pub fn peak_get_range_in_flight(&self) -> u64 {
480 self.peak_get_range.load(Ordering::SeqCst)
481 }
482 pub fn reset_peak_get_range_in_flight(&self) {
483 self.peak_get_range.store(0, Ordering::SeqCst);
484 }
485
486 pub fn inject_head_failures(&self, n: u64) {
493 self.fail_head_count.store(n, Ordering::SeqCst);
494 }
495
496 pub fn inject_epoch_renew_failures(&self, n: u64) {
499 self.fail_epoch_renew_count.store(n, Ordering::SeqCst);
500 }
501
502 pub fn inject_put_lands_then_fails(&self, key_contains: &str, n: u64) {
515 *self.fail_put_after_land_key.lock().unwrap() = key_contains.to_string();
516 self.fail_put_after_land_count.store(n, Ordering::SeqCst);
517 }
518
519 pub fn inject_get_range_stall(&self, ms: u64) {
522 self.stall_next_get_range_ms.store(ms, Ordering::SeqCst);
523 }
524
525 pub fn inject_corrupt_body(&self, key: &str, mutate: impl FnOnce(&mut Vec<u8>)) {
532 let mut inner = self.inner.lock().unwrap();
533 let chain = inner.chains.get_mut(key).expect("corrupting a key that exists");
534 let obj = chain.versions.last_mut().expect("a version to corrupt");
535 let mut b = obj.bytes.to_vec();
536 mutate(&mut b);
537 obj.bytes = Bytes::from(b);
538 }
539
540 pub fn raw_put(&self, key: &str, bytes: Bytes, meta: Vec<(String, String)>) -> ObjectMeta {
543 let obj = StoredObject {
544 etag: put_etag(&bytes),
545 crc64: crc64_nvme(&bytes),
546 meta: meta.into_iter().collect(),
547 last_modified_unix: now_unix(),
548 bytes,
549 version_id: String::new(),
550 deleted: false,
551 };
552 let mut inner = self.inner.lock().unwrap();
553 let vid = inner.push(key, obj);
554 let mut m = inner.current(key).map(|o| o.to_meta()).expect("just pushed");
555 m.version_id = Some(vid);
556 m
557 }
558
559 pub fn redeem_presigned_put(
577 &self,
578 url: &str,
579 headers: &BTreeMap<String, String>,
580 body: Bytes,
581 ) -> StoreResult<ObjectMeta> {
582 let rest = url
583 .strip_prefix("memory://presigned-put/")
584 .ok_or_else(|| StoreError::Other(format!("not a memory presigned PUT: {url}")))?;
585 let (key, query) = rest.split_once('?').unwrap_or((rest, ""));
586 let presented: BTreeMap<String, &String> =
587 headers.iter().map(|(k, v)| (k.to_ascii_lowercase(), v)).collect();
588 for param in query.split('&') {
589 if let Some((name, want)) = param.split_once('=').and_then(|(k, v)| Some((k.strip_prefix("h.")?, v))) {
590 if presented.get(name).map(|v| v.as_str()) != Some(want) {
591 return Err(StoreError::Auth(format!(
592 "SignatureDoesNotMatch: signed header {name} not presented as signed"
593 )));
594 }
595 }
596 }
597 if let Some(sum) = presented.get("x-amz-checksum-sha256") {
598 let actual = crate::base64_std(&Sha256::digest(&body));
599 if **sum != actual {
600 return Err(StoreError::ChecksumMismatch(format!(
601 "BadDigest: {key}: x-amz-checksum-sha256 {sum} but the body hashes to {actual}"
602 )));
603 }
604 }
605 if presented.get("if-none-match").map(|v| v.as_str()) == Some("*")
606 && self.inner.lock().unwrap().current(key).is_some()
607 {
608 return Err(StoreError::PreconditionFailed(format!("{key} exists")));
609 }
610 Ok(self.raw_put(key, body, Vec::new()))
611 }
612
613 pub fn expire_noncurrent(&self, older_than_secs: u64) -> Vec<(String, String)> {
629 let now = now_unix();
630 let mut inner = self.inner.lock().unwrap();
631 let mut reaped = vec![];
632 let keys: Vec<String> = inner.chains.keys().cloned().collect();
633 for k in keys {
634 let Some(chain) = inner.chains.get_mut(&k) else { continue };
635 let last = chain.versions.len().saturating_sub(1);
636 let doomed: Vec<String> = chain
637 .versions
638 .iter()
639 .enumerate()
640 .filter(|(i, o)| {
641 *i != last && now.saturating_sub(o.last_modified_unix) >= older_than_secs
642 })
643 .map(|(_, o)| o.version_id.clone())
644 .collect();
645 for vid in doomed {
646 chain.versions.retain(|o| o.version_id != vid);
647 reaped.push((k.clone(), vid));
648 }
649 }
650 reaped
651 }
652
653 pub fn version_count(&self, key: &str) -> usize {
658 self.inner.lock().unwrap().chains.get(key).map(|c| c.versions.len()).unwrap_or(0)
659 }
660
661 pub fn raw_complete_upload(&self, upload_id: &str) -> StoreResult<ObjectMeta> {
667 let mut inner = self.inner.lock().unwrap();
668 let Some(mpu) = inner.uploads.remove(upload_id) else {
669 return Err(StoreError::NoSuchUpload(format!("upload {}", upload_id)));
670 };
671 let mut bytes = Vec::new();
672 let n_parts = mpu.parts.len().max(1);
673 for part in mpu.parts.values() {
674 bytes.extend_from_slice(part);
675 }
676 let bytes = Bytes::from(bytes);
677 let obj = StoredObject {
678 etag: mpu_etag(&bytes, n_parts),
679 crc64: crc64_nvme(&bytes),
680 meta: HashMap::new(),
681 last_modified_unix: now_unix(),
682 bytes,
683 version_id: String::new(),
684 deleted: false,
685 };
686 let mkey = mpu.key.clone();
687 let vid = inner.push(&mkey, obj);
688 let mut m = inner.current(&mkey).expect("just pushed").to_meta();
689 m.version_id = Some(vid);
690 Ok(m)
691 }
692
693 pub fn raw_begin_upload(&self, key: &str) -> String {
695 let id = format!("mpu-{}", self.upload_seq.fetch_add(1, Ordering::SeqCst));
696 self.inner.lock().unwrap().uploads.insert(
697 id.clone(),
698 Mpu { key: key.to_string(), parts: BTreeMap::new(), initiated_unix: now_unix() },
699 );
700 id
701 }
702
703 fn check_condition(
704 &self,
705 existing: Option<&StoredObject>,
706 condition: &PutCondition,
707 ) -> StoreResult<()> {
708 match (condition, existing) {
709 (PutCondition::IfMatch(want), Some(o)) if o.etag == *want => Ok(()),
710 (PutCondition::IfMatch(_), Some(_)) => {
711 Err(StoreError::PreconditionFailed("If-Match: etag differs".into()))
712 }
713 (PutCondition::IfMatch(_), None) if self.if_match_missing_412.load(Ordering::SeqCst) => {
714 Err(StoreError::PreconditionFailed("If-Match: no object".into()))
715 }
716 (PutCondition::IfMatch(_), None) => Err(StoreError::NotFound("NoSuchKey: If-Match on a missing key".into())),
717 (PutCondition::IfNoneMatchAny, None) => Ok(()),
718 (PutCondition::IfNoneMatchAny, Some(_)) => {
719 Err(StoreError::PreconditionFailed("If-None-Match: object exists".into()))
720 }
721 (PutCondition::Unconditional, _) => Ok(()),
722 }
723 }
724}
725
726fn put_etag(bytes: &[u8]) -> String {
727 let mut h = Sha256::new();
728 h.update(bytes);
729 format!("\"{:x}\"", h.finalize())
730}
731
732fn mpu_etag(bytes: &[u8], parts: usize) -> String {
736 let mut h = Sha256::new();
737 h.update(bytes);
738 format!("\"{:x}-{}\"", h.finalize(), parts)
739}
740
741#[derive(serde::Serialize, serde::Deserialize)]
742struct EpochBody {
743 holder_id: String,
744 epoch: u64,
745 #[serde(default)]
747 released: bool,
748 #[serde(default, skip_serializing_if = "Option::is_none")]
750 echo: Option<String>,
751 #[serde(default, skip_serializing_if = "Vec::is_empty")]
753 waiters: Vec<String>,
754 #[serde(default, skip_serializing_if = "Option::is_none")]
755 handoff: Option<String>,
756}
757
758impl MemoryStore {
759 fn epoch_store(&self, key: &str, body: EpochBody, condition: &PutCondition) -> StoreResult<String> {
764 let bytes = Bytes::from(serde_json::to_vec(&body).unwrap());
765 let mut inner = self.inner.lock().unwrap();
766 self.check_condition(inner.current(key), condition)?;
767 let mut salted = bytes.to_vec();
768 salted.extend_from_slice(&self.upload_seq.fetch_add(1, Ordering::SeqCst).to_be_bytes());
769 let obj = StoredObject {
770 etag: put_etag(&salted),
771 crc64: crc64_nvme(&bytes),
772 meta: HashMap::new(),
773 last_modified_unix: now_unix(),
774 bytes,
775 version_id: String::new(),
776 deleted: false,
777 };
778 let token = obj.etag.clone();
779 inner.push(key, obj);
780 Ok(token)
781 }
782
783 fn bump(&self, op: &'static str) {
784 *self.ops.lock().unwrap().entry(op).or_insert(0) += 1;
785 }
786 pub fn op_counts(&self) -> std::collections::BTreeMap<&'static str, u64> {
788 self.ops.lock().unwrap().clone()
789 }
790 pub fn reset_op_counts(&self) {
791 self.ops.lock().unwrap().clear();
792 }
793 pub fn total_ops(&self) -> u64 {
794 self.ops.lock().unwrap().values().sum()
795 }
796}
797
798#[async_trait]
799impl ObjectStore for MemoryStore {
800 fn request_counts(&self) -> Option<crate::RequestCounts> {
801 let mut c = crate::RequestCounts::default();
804 for (op, n) in self.op_counts() {
805 match op {
806 "get_whole" | "get_range" | "get_version" | "epoch_read" | "presign_get" => c.get += n,
807 "head" | "head_version" => c.head += n,
808 "put_whole" | "epoch_acquire" | "epoch_renew" | "epoch_release" | "epoch_handoff"
809 | "epoch_enqueue" | "presign_put" => c.put += n,
810 "copy_object" => c.copy += n,
811 "delete" | "delete_version" => c.delete += n,
812 "list" | "list_versions" | "list_uploads" => c.list += n,
813 "compose_generation" | "compose_inner" | "abort_upload" => c.multipart += n,
814 _ => {}
815 }
816 }
817 Some(c)
818 }
819
820 async fn put_whole(
821 &self,
822 key: &str,
823 body: Bytes,
824 condition: &PutCondition,
825 stamps: &GenerationStamps,
826 crc64: u64,
827 ) -> StoreResult<ObjectMeta> {
828 self.bump("put_whole");
829 self.pace().await;
830 let held = self.hold_puts.load(Ordering::SeqCst)
831 && match self.hold_puts_matching.lock().unwrap().as_deref() {
832 None => true,
833 Some(m) => key.contains(m),
834 };
835 if held {
836 let (tx, rx) = tokio::sync::oneshot::channel();
837 self.held_puts.lock().unwrap().push(tx);
838 let _ = rx.await;
841 }
842 let delay = self.put_whole_delay_ms.load(Ordering::SeqCst);
843 if delay > 0 {
844 tokio::time::sleep(std::time::Duration::from_millis(delay)).await;
845 }
846 let actual = crc64_nvme(&body);
847 if actual != crc64 {
848 return Err(StoreError::ChecksumMismatch(format!(
849 "put {}: claimed {:#x}, content is {:#x}",
850 key, crc64, actual
851 )));
852 }
853 let mut inner = self.inner.lock().unwrap();
854 self.check_condition(inner.current(key), condition)?;
855 let obj = StoredObject {
856 etag: put_etag(&body),
857 crc64,
858 meta: stamps.to_meta().into_iter().collect(),
859 last_modified_unix: now_unix(),
860 bytes: body,
861 version_id: String::new(),
862 deleted: false,
863 };
864 let vid = inner.push(key, obj);
869 let mut m = inner.current(key).expect("just pushed").to_meta();
870 m.version_id = Some(vid);
871 if self.strip_version_ids.load(Ordering::SeqCst) {
872 m.version_id = None;
876 }
877 if self.fail_put_after_land_count.load(Ordering::SeqCst) > 0 {
881 let pat = self.fail_put_after_land_key.lock().unwrap().clone();
882 if (pat.is_empty() || key.contains(pat.as_str()))
883 && self
884 .fail_put_after_land_count
885 .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |v| v.checked_sub(1))
886 .is_ok()
887 {
888 return Err(StoreError::Conflict(format!(
889 "injected: the PUT of {key} landed and the response did not"
890 )));
891 }
892 }
893 Ok(m)
894 }
895
896 async fn copy_object(
897 &self,
898 src_key: &str,
899 src_if_match: Option<&str>,
900 dst_key: &str,
901 condition: &PutCondition,
902 stamps: &GenerationStamps,
903 ) -> StoreResult<ObjectMeta> {
904 self.bump("copy_object");
905 self.pace().await;
906 let mut inner = self.inner.lock().unwrap();
907 let src = inner
908 .current(src_key)
909 .ok_or_else(|| StoreError::NotFound(src_key.to_string()))?;
910 if let Some(want) = src_if_match {
914 if src.etag != want {
915 return Err(StoreError::PreconditionFailed(format!(
916 "copy source {src_key} is at {} , not {want}",
917 src.etag
918 )));
919 }
920 }
921 let bytes = src.bytes.clone();
922 let crc64 = src.crc64;
928 self.check_condition(inner.current(dst_key), condition)?;
929 let obj = StoredObject {
930 etag: put_etag(&bytes),
931 crc64,
932 meta: stamps.to_meta().into_iter().collect(),
933 last_modified_unix: now_unix(),
934 bytes,
935 version_id: String::new(),
936 deleted: false,
937 };
938 let vid = inner.push(dst_key, obj);
939 let mut m = inner.current(dst_key).expect("just pushed").to_meta();
940 m.version_id = Some(vid);
941 if self.strip_version_ids.load(Ordering::SeqCst) {
942 m.version_id = None;
943 }
944 Ok(m)
945 }
946
947 async fn compose_generation(&self, spec: &ComposeSpec<'_>) -> StoreResult<ObjectMeta> {
948 self.bump("compose_generation");
949 self.pace().await;
950 if spec.parts.is_empty() {
951 return Err(StoreError::Other("compose: no parts".into()));
952 }
953 if spec.parts.len() > self.max_parts {
954 return Err(StoreError::Other(format!(
955 "compose: {} parts exceeds the backend maximum {}",
956 spec.parts.len(),
957 self.max_parts
958 )));
959 }
960 let upload_id = self.raw_begin_upload(spec.key);
961 let result = self.compose_inner(spec, &upload_id).await;
965 if result.is_err() && !self.leave_orphan.swap(false, Ordering::SeqCst) {
966 let mut inner = self.inner.lock().unwrap();
967 inner.uploads.remove(&upload_id);
968 }
969 result
970 }
971
972 async fn head(&self, key: &str) -> StoreResult<ObjectMeta> {
973 self.bump("head");
974 self.pace().await;
975 if self
976 .fail_head_count
977 .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |c| c.checked_sub(1))
978 .is_ok()
979 {
980 return Err(StoreError::Other("injected head failure".into()));
981 }
982 self.inner
983 .lock()
984 .unwrap()
985 .current(key)
986 .map(|o| o.to_meta())
987 .ok_or_else(|| StoreError::NotFound(key.into()))
988 }
989
990 async fn presign_get(&self, key: &str, ttl_secs: u64) -> StoreResult<String> {
997 self.bump("presign_get");
998 let inner = self.inner.lock().unwrap();
999 if inner.current(key).is_none() {
1000 return Err(StoreError::NotFound(key.into()));
1001 }
1002 Ok(format!("memory://presigned/{key}?ttl={ttl_secs}"))
1003 }
1004
1005 async fn presign_put(
1011 &self,
1012 key: &str,
1013 ttl_secs: u64,
1014 sha256: &[u8; 32],
1015 ) -> StoreResult<PresignedPut> {
1016 self.bump("presign_put");
1017 let headers: BTreeMap<String, String> = [
1018 ("x-amz-checksum-sha256".to_string(), crate::base64_std(sha256)),
1019 ("if-none-match".to_string(), "*".to_string()),
1020 ]
1021 .into_iter()
1022 .collect();
1023 let mut url = format!("memory://presigned-put/{key}?ttl={ttl_secs}");
1024 for (k, v) in &headers {
1025 url.push_str(&format!("&h.{k}={v}"));
1026 }
1027 Ok(PresignedPut { url, headers })
1028 }
1029
1030 async fn get_whole(
1031 &self,
1032 key: &str,
1033 if_match: Option<&str>,
1034 ) -> StoreResult<(ObjectMeta, Bytes)> {
1035 self.bump("get_whole");
1036 self.pace().await;
1037 let inner = self.inner.lock().unwrap();
1038 let o = inner
1039 .current(key)
1040 .ok_or_else(|| StoreError::NotFound(key.into()))?;
1041 if let Some(want) = if_match {
1042 if o.etag != want {
1043 return Err(StoreError::PreconditionFailed(format!(
1044 "get {}: etag {} != {}",
1045 key, o.etag, want
1046 )));
1047 }
1048 }
1049 Ok((o.to_meta(), o.bytes.clone()))
1050 }
1051
1052 async fn get_range_segments(
1059 &self,
1060 key: &str,
1061 offset: u64,
1062 len: u64,
1063 if_match: &str,
1064 ) -> StoreResult<Vec<Bytes>> {
1065 let whole = self.get_range(key, offset, len, if_match).await?;
1066 let mut segs: Vec<Bytes> = Vec::new();
1070 let (mut at, mut step) = (0usize, 1usize);
1071 while at < whole.len() {
1072 let n = step.min(whole.len() - at);
1073 segs.push(whole.slice(at..at + n)); at += n;
1075 step = (step.saturating_mul(3)).min(64 * 1024);
1076 }
1077 if segs.is_empty() {
1078 segs.push(whole);
1079 }
1080 Ok(segs)
1081 }
1082
1083 async fn get_range(
1084 &self,
1085 key: &str,
1086 offset: u64,
1087 len: u64,
1088 if_match: &str,
1089 ) -> StoreResult<Bytes> {
1090 self.bump("get_range");
1091 self.pace().await;
1092 let _in_flight = {
1093 let now = self.inflight_get_range.fetch_add(1, Ordering::SeqCst) + 1;
1094 self.peak_get_range.fetch_max(now, Ordering::SeqCst);
1095 InFlight(&self.inflight_get_range)
1096 };
1097 if self
1099 .fail_get_range_count
1100 .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |c| c.checked_sub(1))
1101 .is_ok()
1102 {
1103 return Err(StoreError::Other("injected get_range failure".into()));
1104 }
1105 let stall = self.stall_next_get_range_ms.swap(0, Ordering::SeqCst);
1106 if stall > 0 {
1107 tokio::time::sleep(std::time::Duration::from_millis(stall)).await;
1108 }
1109 let delay = self.get_range_delay_ms.load(Ordering::SeqCst);
1110 if delay > 0 {
1111 tokio::time::sleep(std::time::Duration::from_millis(delay)).await;
1112 }
1113 let inner = self.inner.lock().unwrap();
1114 let o = inner
1115 .current(key)
1116 .ok_or_else(|| StoreError::NotFound(key.into()))?;
1117 if o.etag != if_match {
1118 return Err(StoreError::PreconditionFailed(format!(
1119 "get_range {}: etag {} != {}",
1120 key, o.etag, if_match
1121 )));
1122 }
1123 let start = offset.min(o.bytes.len() as u64) as usize;
1124 let end = (offset + len).min(o.bytes.len() as u64) as usize;
1125 Ok(o.bytes.slice(start..end))
1126 }
1127
1128 async fn list(&self, prefix: &str) -> StoreResult<Vec<ListedObject>> {
1129 self.bump("list");
1130 self.pace().await;
1131 let denied = self.deny_list_matching.lock().unwrap().as_deref().is_some_and(|m| prefix.contains(m));
1132 if denied {
1133 return Err(StoreError::Auth(format!("injected: no listing of {prefix}")));
1134 }
1135 Ok(self
1136 .inner
1137 .lock()
1138 .unwrap()
1139 .chains
1140 .range(prefix.to_string()..)
1141 .take_while(|(k, _)| k.starts_with(prefix))
1142 .filter_map(|(k, c)| {
1145 c.current().map(|o| ListedObject {
1146 key: k.clone(),
1147 size: o.bytes.len() as u64,
1148 etag: o.etag.clone(),
1149 last_modified_unix: Some(o.last_modified_unix),
1150 })
1151 })
1152 .collect())
1153 }
1154
1155 async fn delete(&self, key: &str) -> StoreResult<()> {
1156 self.bump("delete");
1157 self.pace().await;
1158 self.inner.lock().unwrap().push_delete_marker(key);
1159 Ok(())
1160 }
1161
1162 async fn delete_if_match(&self, key: &str, etag: &str) -> StoreResult<()> {
1163 self.bump("delete");
1165 self.pace().await;
1166 let mut inner = self.inner.lock().unwrap();
1167 let refused = match inner.current(key) {
1168 None => Some(StoreError::NotFound(key.to_string())),
1169 Some(o) if o.etag != etag => Some(StoreError::PreconditionFailed(format!(
1170 "{key}: If-Match {etag} but the current etag is {}",
1171 o.etag
1172 ))),
1173 Some(_) => None,
1174 };
1175 if let Some(e) = refused {
1176 return Err(e);
1177 }
1178 inner.push_delete_marker(key);
1179 Ok(())
1180 }
1181
1182 async fn delete_many(&self, keys: &[String]) -> StoreResult<DeleteManyReport> {
1185 let mut report = DeleteManyReport::default();
1186 for chunk in keys.chunks(1000) {
1187 self.bump("delete");
1188 self.pace().await;
1189 let mut inner = self.inner.lock().unwrap();
1190 for key in chunk {
1191 inner.push_delete_marker(key);
1192 report.deleted += 1;
1193 }
1194 }
1195 Ok(report)
1196 }
1197
1198 async fn head_version(&self, key: &str, version_id: &str) -> StoreResult<ObjectMeta> {
1199 self.bump("head_version");
1200 self.pace().await;
1201 self.inner
1202 .lock()
1203 .unwrap()
1204 .version(key, version_id)
1205 .map(|o| o.to_meta())
1206 .ok_or_else(|| StoreError::NotFound(format!("{key}?versionId={version_id}")))
1207 }
1208
1209 async fn get_version(&self, key: &str, version_id: &str) -> StoreResult<(ObjectMeta, Bytes)> {
1210 self.bump("get_version");
1211 self.pace().await;
1212 let inner = self.inner.lock().unwrap();
1213 let o = inner
1214 .version(key, version_id)
1215 .ok_or_else(|| StoreError::NotFound(format!("{key}?versionId={version_id}")))?;
1216 Ok((o.to_meta(), o.bytes.clone()))
1217 }
1218
1219 async fn delete_version(&self, key: &str, version_id: &str) -> StoreResult<()> {
1220 self.bump("delete_version");
1221 self.pace().await;
1222 self.inner.lock().unwrap().remove_version(key, version_id);
1225 Ok(())
1226 }
1227
1228 async fn list_versions(&self, prefix: &str) -> StoreResult<Vec<ListedVersion>> {
1229 self.bump("list_versions");
1230 self.pace().await;
1231 let inner = self.inner.lock().unwrap();
1232 let mut out = vec![];
1233 for (k, chain) in inner.chains.range(prefix.to_string()..) {
1234 if !k.starts_with(prefix) {
1235 break;
1236 }
1237 let last = chain.versions.len().saturating_sub(1);
1238 for (i, o) in chain.versions.iter().enumerate() {
1239 out.push(ListedVersion {
1240 key: k.clone(),
1241 version_id: o.version_id.clone(),
1242 etag: o.etag.clone(),
1243 size: o.bytes.len() as u64,
1244 is_current: i == last,
1245 is_delete_marker: o.deleted,
1246 last_modified_unix: Some(o.last_modified_unix),
1247 });
1248 }
1249 }
1250 Ok(out)
1251 }
1252
1253 async fn list_uploads(&self, prefix: &str) -> StoreResult<Vec<PendingUpload>> {
1254 self.bump("list_uploads");
1255 self.pace().await;
1256 let inner = self.inner.lock().unwrap();
1257 let mut v: Vec<PendingUpload> = inner
1258 .uploads
1259 .iter()
1260 .filter(|(_, m)| m.key.starts_with(prefix))
1261 .map(|(id, m)| PendingUpload {
1262 key: m.key.clone(),
1263 upload_id: id.clone(),
1264 initiated_unix: Some(m.initiated_unix),
1265 })
1266 .collect();
1267 v.sort_by(|a, b| a.upload_id.cmp(&b.upload_id));
1268 Ok(v)
1269 }
1270
1271 async fn abort_upload(&self, _key: &str, upload_id: &str) -> StoreResult<()> {
1272 self.bump("abort_upload");
1273 self.pace().await;
1274 self.inner.lock().unwrap().uploads.remove(upload_id);
1276 Ok(())
1277 }
1278
1279 async fn bootstrap(&self, _prefix: &str) -> StoreResult<BootstrapReport> {
1280 self.bump("bootstrap");
1281 self.pace().await;
1282 Ok(BootstrapReport {
1283 notes: vec!["memory store: no bucket posture to verify".into()],
1284 ..Default::default()
1285 })
1286 }
1287
1288 async fn lifecycle_rules(&self) -> StoreResult<Vec<LifecycleView>> {
1289 self.bump("lifecycle_rules");
1290 self.pace().await;
1291 Ok(self.inner.lock().unwrap().lifecycle.clone())
1292 }
1293
1294 async fn ensure_noncurrent_retention(
1295 &self,
1296 prefix: &str,
1297 days: u64,
1298 ) -> StoreResult<RetentionOutcome> {
1299 self.bump("ensure_noncurrent_retention");
1300 self.pace().await;
1301 if self.fail_lifecycle_writes.load(Ordering::SeqCst) {
1302 return Err(StoreError::Other("lifecycle writes are denied".into()));
1303 }
1304 let rule_id = format!("flint-lean-noncurrent-{}", prefix.trim_end_matches('/').replace('/', "-"));
1305 let mut inner = self.inner.lock().unwrap();
1306 if let Some(r) = inner
1309 .lifecycle
1310 .iter()
1311 .find(|r| r.enabled && r.prefix == prefix && r.noncurrent_days == Some(days))
1312 {
1313 return Ok(RetentionOutcome {
1314 rule_id: r.id.clone(),
1315 noncurrent_days: days,
1316 created: false,
1317 });
1318 }
1319 inner.lifecycle.retain(|r| r.id != rule_id);
1320 inner.lifecycle.push(LifecycleView {
1321 id: rule_id.clone(),
1322 enabled: true,
1323 prefix: prefix.to_string(),
1324 noncurrent_days: Some(days),
1325 expired_delete_marker: true,
1326 });
1327 Ok(RetentionOutcome { rule_id, noncurrent_days: days, created: true })
1328 }
1329
1330 async fn epoch_read(&self, key: &str) -> StoreResult<Option<EpochState>> {
1331 self.bump("epoch_read");
1332 self.pace().await;
1333 let inner = self.inner.lock().unwrap();
1334 let Some(o) = inner.current(key) else {
1335 return Ok(None);
1336 };
1337 let body: EpochBody = serde_json::from_slice(&o.bytes)
1338 .map_err(|e| StoreError::Other(format!("epoch body: {}", e)))?;
1339 Ok(Some(EpochState {
1340 holder_id: body.holder_id,
1341 epoch: body.epoch,
1342 token: o.etag.clone(),
1343 last_renew_unix: Some(o.last_modified_unix),
1344 released: body.released,
1345 echo: body.echo,
1346 waiters: body.waiters,
1347 handoff: body.handoff,
1348 }))
1349 }
1350
1351 async fn epoch_acquire(
1352 &self,
1353 key: &str,
1354 holder_id: &str,
1355 supersede: Option<&EpochState>,
1356 ) -> StoreResult<EpochLease> {
1357 self.bump("epoch_acquire");
1358 self.pace().await;
1359 let epoch = supersede.map_or(1, |s| s.epoch + 1);
1360 let waiters: Vec<String> = supersede
1361 .map(|s| s.waiters.iter().filter(|w| w.as_str() != holder_id).cloned().collect())
1362 .unwrap_or_default();
1363 let condition = match supersede {
1364 None => PutCondition::IfNoneMatchAny,
1365 Some(s) => PutCondition::IfMatch(s.token.clone()),
1366 };
1367 let token = self.epoch_store(
1368 key,
1369 EpochBody {
1370 holder_id: holder_id.into(),
1371 epoch,
1372 released: false,
1373 echo: None,
1374 waiters: waiters.clone(),
1375 handoff: None,
1376 },
1377 &condition,
1378 )?;
1379 Ok(EpochLease { holder_id: holder_id.into(), epoch, token, waiters })
1380 }
1381
1382 async fn epoch_renew(
1383 &self,
1384 key: &str,
1385 lease: &EpochLease,
1386 echo: Option<&str>,
1387 ) -> StoreResult<EpochLease> {
1388 self.bump("epoch_renew");
1389 self.pace().await;
1390 if self
1391 .fail_epoch_renew_count
1392 .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |c| c.checked_sub(1))
1393 .is_ok()
1394 {
1395 return Err(StoreError::Other("injected renew failure: store unreachable".into()));
1396 }
1397 let token = self.epoch_store(
1398 key,
1399 EpochBody {
1400 holder_id: lease.holder_id.clone(),
1401 epoch: lease.epoch,
1402 released: false,
1403 echo: echo.map(|e| e.to_string()),
1404 waiters: lease.waiters.clone(),
1405 handoff: None,
1406 },
1407 &PutCondition::IfMatch(lease.token.clone()),
1408 )?;
1409 Ok(EpochLease {
1410 holder_id: lease.holder_id.clone(),
1411 epoch: lease.epoch,
1412 token,
1413 waiters: lease.waiters.clone(),
1414 })
1415 }
1416
1417 async fn epoch_release(&self, key: &str, lease: &EpochLease) -> StoreResult<()> {
1418 self.bump("epoch_release");
1419 self.pace().await;
1420 self.epoch_store(
1425 key,
1426 EpochBody {
1427 holder_id: lease.holder_id.clone(),
1428 epoch: lease.epoch,
1429 released: true,
1430 echo: None,
1431 waiters: lease.waiters.clone(),
1432 handoff: None,
1433 },
1434 &PutCondition::IfMatch(lease.token.clone()),
1435 )?;
1436 Ok(())
1437 }
1438
1439 async fn epoch_handoff(
1440 &self,
1441 key: &str,
1442 lease: &EpochLease,
1443 echo: Option<&str>,
1444 ) -> StoreResult<()> {
1445 self.bump("epoch_handoff");
1446 self.pace().await;
1447 let mut rest = lease.waiters.clone();
1448 let head = if rest.is_empty() { None } else { Some(rest.remove(0)) };
1449 self.epoch_store(
1450 key,
1451 EpochBody {
1452 holder_id: lease.holder_id.clone(),
1453 epoch: lease.epoch,
1454 released: true,
1455 echo: echo.map(|e| e.to_string()),
1456 waiters: rest,
1457 handoff: head,
1458 },
1459 &PutCondition::IfMatch(lease.token.clone()),
1460 )?;
1461 Ok(())
1462 }
1463
1464 async fn epoch_enqueue(
1465 &self,
1466 key: &str,
1467 observed: &EpochState,
1468 holder_id: &str,
1469 ) -> StoreResult<EpochState> {
1470 self.bump("epoch_enqueue");
1471 self.pace().await;
1472 let mut waiters = observed.waiters.clone();
1473 if !waiters.iter().any(|w| w == holder_id) {
1474 waiters.push(holder_id.to_string());
1475 }
1476 let token = self.epoch_store(
1477 key,
1478 EpochBody {
1479 holder_id: observed.holder_id.clone(),
1480 epoch: observed.epoch,
1481 released: observed.released,
1482 echo: observed.echo.clone(),
1483 waiters: waiters.clone(),
1484 handoff: observed.handoff.clone(),
1485 },
1486 &PutCondition::IfMatch(observed.token.clone()),
1487 )?;
1488 Ok(EpochState {
1489 holder_id: observed.holder_id.clone(),
1490 epoch: observed.epoch,
1491 token,
1492 last_renew_unix: Some(now_unix()),
1493 echo: observed.echo.clone(),
1494 released: observed.released,
1495 waiters,
1496 handoff: observed.handoff.clone(),
1497 })
1498 }
1499
1500 fn min_part_size(&self) -> u64 {
1501 self.min_part
1502 }
1503
1504 fn max_parts(&self) -> usize {
1505 self.max_parts
1506 }
1507
1508 fn upload_gate(&self) -> Option<std::sync::Arc<crate::gate::ByteGate>> {
1509 self.upload_gate.clone()
1510 }
1511}
1512
1513impl MemoryStore {
1514 async fn stage_one_part(
1520 &self,
1521 spec: &ComposeSpec<'_>,
1522 upload_id: &str,
1523 i: usize,
1524 p: &PartSource,
1525 ) -> StoreResult<()> {
1526 match p {
1527 PartSource::Local { offset, len } => {
1528 let _permit = match &self.upload_gate {
1537 Some(g) => Some(g.acquire(*len).await),
1538 None => None,
1539 };
1540 let file = spec.local.clone().ok_or_else(|| {
1541 StoreError::Other(format!(
1542 "compose of {}: a Local part with no open descriptor",
1543 spec.key
1544 ))
1545 })?;
1546 let (offset, len) = (*offset, *len);
1547 let bytes = tokio::task::spawn_blocking(move || read_local(&file, offset, len))
1548 .await
1549 .map_err(|e| StoreError::Other(format!("local read join: {e}")))??;
1550 self.pace().await;
1555 self.land_part(spec, upload_id, i, len, bytes)
1556 }
1557 PartSource::BaseCopy { offset, len } => {
1558 let want = spec.base_etag.as_deref().ok_or_else(|| {
1559 StoreError::Other("compose: BaseCopy without base_etag".into())
1560 })?;
1561 let bytes = {
1562 let inner = self.inner.lock().unwrap();
1563 let base_key = spec.base_key.unwrap_or(spec.key);
1564 let base = inner.current(base_key).ok_or_else(|| {
1565 StoreError::PreconditionFailed("copy-source: no base object".into())
1566 })?;
1567 if base.etag != want {
1568 return Err(StoreError::PreconditionFailed(
1569 "copy-source-if-match: base etag differs".into(),
1570 ));
1571 }
1572 let s = *offset as usize;
1573 let e = (*offset + *len) as usize;
1574 if e > base.bytes.len() {
1575 return Err(StoreError::Other(format!(
1576 "copy-source range [{}, {}) beyond base size {}",
1577 s,
1578 e,
1579 base.bytes.len()
1580 )));
1581 }
1582 base.bytes.slice(s..e)
1583 };
1584 self.land_part(spec, upload_id, i, *len, bytes)
1585 }
1586 }
1587 }
1588
1589 fn land_part(
1592 &self,
1593 spec: &ComposeSpec<'_>,
1594 upload_id: &str,
1595 i: usize,
1596 want_len: u64,
1597 bytes: Bytes,
1598 ) -> StoreResult<()> {
1599 if bytes.len() as u64 != want_len {
1600 return Err(StoreError::Other(format!("compose: part {} short read", i)));
1601 }
1602 spec.note_progress(bytes.len() as u64);
1603 self.inner
1604 .lock()
1605 .unwrap()
1606 .uploads
1607 .get_mut(upload_id)
1608 .ok_or_else(|| StoreError::NoSuchUpload(upload_id.into()))?
1609 .parts
1610 .insert(i, bytes);
1611 Ok(())
1612 }
1613
1614 async fn compose_inner(
1615 &self,
1616 spec: &ComposeSpec<'_>,
1617 upload_id: &str,
1618 ) -> StoreResult<ObjectMeta> {
1619 self.bump("compose_inner");
1620 if spec.crc64.is_none()
1623 && spec.parts.iter().any(|p| matches!(p, PartSource::BaseCopy { .. }))
1624 {
1625 return Err(StoreError::Other(
1626 "compose: the checksum cannot be accumulated over a copied part; supply crc64".into(),
1627 ));
1628 }
1629 let mut expect = 0u64;
1633 for (i, p) in spec.parts.iter().enumerate() {
1634 let (off, len) = match p {
1635 PartSource::Local { offset, len } | PartSource::BaseCopy { offset, len } => {
1636 (*offset, *len)
1637 }
1638 };
1639 if off != expect {
1640 return Err(StoreError::Other(format!(
1641 "compose: part {} at offset {} but object is contiguous to {}",
1642 i, off, expect
1643 )));
1644 }
1645 if len < self.min_part && i + 1 != spec.parts.len() {
1646 return Err(StoreError::Other(format!(
1647 "compose: part {} len {} under the backend minimum {}",
1648 i, len, self.min_part
1649 )));
1650 }
1651 expect = off + len;
1652 }
1653
1654 {
1661 use futures::stream::StreamExt;
1662 let par = self.part_parallelism.max(1);
1663 let mut futs = Vec::with_capacity(spec.parts.len());
1664 for (i, p) in spec.parts.iter().enumerate() {
1665 futs.push(self.stage_one_part(spec, upload_id, i, p));
1666 }
1667 let staged: Vec<StoreResult<()>> =
1668 futures::stream::iter(futs).buffer_unordered(par).collect().await;
1669 staged.into_iter().collect::<StoreResult<Vec<()>>>()?;
1670 }
1671 let mut assembled = Vec::with_capacity(expect as usize);
1673 {
1674 let inner = self.inner.lock().unwrap();
1675 let up = inner
1676 .uploads
1677 .get(upload_id)
1678 .ok_or_else(|| StoreError::NoSuchUpload(upload_id.into()))?;
1679 for i in 0..spec.parts.len() {
1680 let bytes = up.parts.get(&i).ok_or_else(|| {
1681 StoreError::Other(format!("compose: part {} was staged but did not land", i))
1682 })?;
1683 assembled.extend_from_slice(bytes);
1684 }
1685 }
1686
1687 let actual = crc64_nvme(&assembled);
1697 if let Some(claimed) = spec.crc64 {
1698 if actual != claimed {
1699 return Err(StoreError::ChecksumMismatch(format!(
1700 "compose {}: claimed {:#x}, content is {:#x}",
1701 spec.key, claimed, actual
1702 )));
1703 }
1704 }
1705
1706 match self.inject.swap(INJECT_NONE, Ordering::SeqCst) {
1707 INJECT_CRASH_BEFORE_COMPLETE => {
1708 self.leave_orphan.store(true, Ordering::SeqCst);
1712 return Err(StoreError::Other("injected: crash before Complete".into()));
1713 }
1714 INJECT_TORN_COMPLETE => {
1715 self.finish_complete(spec, upload_id, assembled)?;
1717 return Err(StoreError::Other("injected: Complete response lost".into()));
1718 }
1719 INJECT_SWEPT => {
1720 return Err(StoreError::NoSuchUpload(format!("upload {upload_id} (injected: swept)")));
1721 }
1722 _ => {}
1723 }
1724
1725 let meta = self.finish_complete(spec, upload_id, assembled)?;
1726 Ok(meta)
1727 }
1728
1729 fn finish_complete(
1730 &self,
1731 spec: &ComposeSpec<'_>,
1732 upload_id: &str,
1733 assembled: Vec<u8>,
1734 ) -> StoreResult<ObjectMeta> {
1735 let actual = crc64_nvme(&assembled);
1736 if let Some(claimed) = spec.crc64 {
1737 if actual != claimed {
1738 return Err(StoreError::ChecksumMismatch(format!(
1739 "compose {}: claimed {:#x}, assembled is {:#x}",
1740 spec.key, claimed, actual
1741 )));
1742 }
1743 }
1744 let mut inner = self.inner.lock().unwrap();
1745 if inner.uploads.remove(upload_id).is_none() {
1746 return Err(StoreError::NoSuchUpload(upload_id.into()));
1748 }
1749 self.check_condition(inner.current(spec.key), &spec.condition)?;
1750 let parts = spec.parts.len();
1751 let bytes = Bytes::from(assembled);
1752 let obj = StoredObject {
1753 etag: mpu_etag(&bytes, parts),
1754 crc64: actual,
1755 meta: spec.stamps.to_meta().into_iter().collect(),
1756 last_modified_unix: now_unix(),
1757 bytes,
1758 version_id: String::new(),
1759 deleted: false,
1760 };
1761 let vid = inner.push(spec.key, obj);
1762 let mut m = inner.current(spec.key).expect("just pushed").to_meta();
1763 m.version_id = Some(vid);
1764 Ok(m)
1765 }
1766}
1767
1768fn read_local(file: &std::sync::Arc<std::fs::File>, offset: u64, len: u64) -> StoreResult<Bytes> {
1772 use std::os::unix::fs::FileExt;
1773 let mut buf = vec![0u8; len as usize];
1774 file.read_exact_at(&mut buf, offset)
1775 .map_err(|e| StoreError::Other(format!("local read: {}", e)))?;
1776 Ok(Bytes::from(buf))
1777}
1778
1779#[cfg(test)]
1780mod tests {
1781 use super::*;
1782
1783 fn stamps(generation: u64) -> GenerationStamps {
1784 GenerationStamps { generation, epoch: 1, flush_uuid: format!("u-{}", generation), boundary_source: None, posix: None }
1785 }
1786
1787 #[tokio::test]
1793 async fn delete_many_takes_every_key_tolerates_absent_ones_and_costs_one_request_per_batch() {
1794 let s = MemoryStore::new();
1795 let body = Bytes::from_static(b"bytes");
1796 let crc = crc64_nvme(&body);
1797 for k in ["h/a@1", "h/a@2", "h/b@1"] {
1798 s.put_whole(k, body.clone(), &PutCondition::IfNoneMatchAny, &stamps(1), crc).await.unwrap();
1799 }
1800 s.reset_op_counts();
1801 let keys: Vec<String> = ["h/a@1", "h/b@1", "h/never-existed@9"].iter().map(|k| k.to_string()).collect();
1802 let report = s.delete_many(&keys).await.unwrap();
1803 assert_eq!(report, DeleteManyReport { deleted: 3, failed: vec![] });
1804 assert!(matches!(s.head("h/a@1").await, Err(StoreError::NotFound(_))));
1805 assert!(matches!(s.head("h/b@1").await, Err(StoreError::NotFound(_))));
1806 assert!(s.head("h/a@2").await.is_ok(), "a key the batch did not name must stay");
1807 assert_eq!(s.op_counts().get("delete").copied(), Some(1), "one request for a batch under a thousand");
1808
1809 let big: Vec<String> = (0..2001).map(|i| format!("h/x@{i}")).collect();
1810 s.reset_op_counts();
1811 let report = s.delete_many(&big).await.unwrap();
1812 assert_eq!(report.deleted, 2001);
1813 assert_eq!(s.op_counts().get("delete").copied(), Some(3), "a thousand keys per request");
1814 }
1815
1816 #[tokio::test]
1817 async fn conditional_puts_and_gets_enforce_the_contract() {
1818 let s = MemoryStore::new();
1819 let body = Bytes::from_static(b"gen-1");
1820 let crc = crc64_nvme(&body);
1821
1822 let err = s
1824 .put_whole("k", body.clone(), &PutCondition::IfNoneMatchAny, &stamps(1), crc ^ 1)
1825 .await
1826 .unwrap_err();
1827 assert!(matches!(err, StoreError::ChecksumMismatch(_)));
1828
1829 let m1 = s
1830 .put_whole("k", body.clone(), &PutCondition::IfNoneMatchAny, &stamps(1), crc)
1831 .await
1832 .unwrap();
1833 let err = s
1835 .put_whole("k", body.clone(), &PutCondition::IfNoneMatchAny, &stamps(1), crc)
1836 .await
1837 .unwrap_err();
1838 assert!(matches!(err, StoreError::PreconditionFailed(_)));
1839
1840 let b2 = Bytes::from_static(b"gen-2");
1842 let m2 = s
1843 .put_whole("k", b2.clone(), &PutCondition::IfMatch(m1.etag.clone()), &stamps(2), crc64_nvme(&b2))
1844 .await
1845 .unwrap();
1846 let err = s
1847 .put_whole("k", b2.clone(), &PutCondition::IfMatch(m1.etag.clone()), &stamps(3), crc64_nvme(&b2))
1848 .await
1849 .unwrap_err();
1850 assert!(matches!(err, StoreError::PreconditionFailed(_)));
1851
1852 let (gm, gb) = s.get_whole("k", Some(&m2.etag)).await.unwrap();
1854 assert_eq!(gb, b2);
1855 assert_eq!(GenerationStamps::from_meta(&gm.meta).unwrap().generation, 2);
1856 let err = s.get_whole("k", Some(&m1.etag)).await.unwrap_err();
1857 assert!(matches!(err, StoreError::PreconditionFailed(_)));
1858 }
1859
1860 #[tokio::test]
1861 async fn epoch_cas_lease_lifecycle() {
1862 let s = MemoryStore::new();
1863 const K: &str = "vol/.flint-epoch";
1864
1865 let l1 = s.epoch_acquire(K, "hub-a", None).await.unwrap();
1867 assert_eq!(l1.epoch, 1);
1868 let err = s.epoch_acquire(K, "hub-b", None).await.unwrap_err();
1869 assert!(matches!(err, StoreError::PreconditionFailed(_)));
1870
1871 let l1b = s.epoch_renew(K, &l1, None).await.unwrap();
1874 assert_ne!(l1b.token, l1.token, "renew must rotate the CAS token");
1875 let err = s.epoch_renew(K, &l1, None).await.unwrap_err();
1876 assert!(matches!(err, StoreError::PreconditionFailed(_)), "stale token must be dead");
1877
1878 let observed = s.epoch_read(K).await.unwrap().unwrap();
1882 assert_eq!(observed.holder_id, "hub-a");
1883 let l2 = s.epoch_acquire(K, "hub-b", Some(&observed)).await.unwrap();
1884 assert_eq!(l2.epoch, 2);
1885 let err = s.epoch_renew(K, &l1b, None).await.unwrap_err();
1886 assert!(matches!(err, StoreError::PreconditionFailed(_)), "deposed renew must fail");
1887 let err = s.epoch_release(K, &l1b).await.unwrap_err();
1888 assert!(matches!(err, StoreError::PreconditionFailed(_)), "deposed release must fail");
1889
1890 s.epoch_release(K, &l2).await.unwrap();
1894 let after = s.epoch_read(K).await.unwrap().expect("the cell must survive a release");
1895 assert!(after.released, "a clean shutdown marks the cell released");
1896 assert_eq!(after.epoch, 2, "and the epoch number is preserved");
1897 assert_eq!(after.holder_id, "hub-b");
1898
1899 let l3 = s.epoch_acquire(K, "hub-c", Some(&after)).await.unwrap();
1902 assert_eq!(l3.epoch, 3, "numbering continues, it does not reset");
1903 assert!(
1904 !s.epoch_read(K).await.unwrap().unwrap().released,
1905 "a fresh claim clears the released mark"
1906 );
1907 }
1908
1909 #[tokio::test]
1917 async fn epoch_queue_hands_the_cell_to_the_head_and_keeps_the_echo() {
1918 const K: &str = "t/.flint/lean/epoch";
1919 let s = MemoryStore::new();
1920 let a = s.epoch_acquire(K, "a", None).await.unwrap();
1921 let seen = s.epoch_read(K).await.unwrap().unwrap();
1922 let seen = s.epoch_enqueue(K, &seen, "b").await.unwrap();
1923 let seen = s.epoch_enqueue(K, &seen, "c").await.unwrap();
1924 let seen = s.epoch_enqueue(K, &seen, "b").await.unwrap();
1925 assert_eq!(seen.waiters, vec!["b", "c"], "a waiter is appended once, in order");
1926 let err = s.epoch_renew(K, &a, None).await.unwrap_err();
1929 assert!(matches!(err, StoreError::PreconditionFailed(_)));
1930 let adopted = EpochLease {
1933 holder_id: "a".into(),
1934 epoch: seen.epoch,
1935 token: seen.token.clone(),
1936 waiters: seen.waiters.clone(),
1937 };
1938 let a = s.epoch_renew(K, &adopted, Some("echo-a")).await.unwrap();
1939 assert_eq!(s.epoch_read(K).await.unwrap().unwrap().waiters, vec!["b", "c"]);
1940 s.epoch_handoff(K, &a, Some("echo-a")).await.unwrap();
1942 let cell = s.epoch_read(K).await.unwrap().unwrap();
1943 assert!(cell.released);
1944 assert_eq!(cell.handoff.as_deref(), Some("b"));
1945 assert_eq!(cell.waiters, vec!["c"]);
1946 assert_eq!(cell.echo.as_deref(), Some("echo-a"), "the last barrier's echo survives the handoff");
1947 let b = s.epoch_acquire(K, "b", Some(&cell)).await.unwrap();
1949 assert_eq!(b.epoch, cell.epoch + 1);
1950 assert_eq!(b.waiters, vec!["c"]);
1951 let cell = s.epoch_read(K).await.unwrap().unwrap();
1952 assert!(!cell.released && cell.handoff.is_none());
1953 assert_eq!(cell.holder_id, "b");
1954 assert_eq!(cell.waiters, vec!["c"]);
1955 s.epoch_handoff(K, &b, None).await.unwrap();
1958 let cell = s.epoch_read(K).await.unwrap().unwrap();
1959 assert_eq!(cell.handoff.as_deref(), Some("c"));
1960 assert!(cell.waiters.is_empty());
1961 let c = s.epoch_acquire(K, "c", Some(&cell)).await.unwrap();
1962 assert!(c.waiters.is_empty());
1963 s.epoch_handoff(K, &c, None).await.unwrap();
1964 let cell = s.epoch_read(K).await.unwrap().unwrap();
1965 assert!(cell.released && cell.handoff.is_none() && cell.waiters.is_empty());
1966 }
1967
1968 #[tokio::test]
1969 async fn compose_mixes_local_and_guarded_base_copy() {
1970 let s = MemoryStore::new();
1971 let base_body = Bytes::from_static(b"AAAABBBB");
1973 let base = s
1974 .put_whole("f", base_body.clone(), &PutCondition::IfNoneMatchAny, &stamps(1), crc64_nvme(&base_body))
1975 .await
1976 .unwrap();
1977 let dir = tempfile::TempDir::new().unwrap();
1979 let local = dir.path().join("f.local");
1980 std::fs::write(&local, b"XXXXBBBB").unwrap();
1981
1982 let spec = ComposeSpec {
1983 progress: None,
1984 key: "f",
1985 local: Some(std::sync::Arc::new(std::fs::File::open(&local).unwrap())),
1986 parts: vec![
1987 PartSource::Local { offset: 0, len: 4 },
1988 PartSource::BaseCopy { offset: 4, len: 4 },
1989 ],
1990 base_key: None,
1991 base_etag: Some(base.etag.clone()),
1992 condition: PutCondition::IfMatch(base.etag.clone()),
1993 stamps: stamps(2),
1994 crc64: Some(crc64_nvme(b"XXXXBBBB")),
1995 };
1996 let m2 = s.compose_generation(&spec).await.unwrap();
1997 let (_, bytes) = s.get_whole("f", Some(&m2.etag)).await.unwrap();
1998 assert_eq!(bytes, Bytes::from_static(b"XXXXBBBB"));
1999 assert!(m2.etag.ends_with("-2\""), "multipart etag must not look like a content hash");
2000
2001 s.raw_put("f", Bytes::from_static(b"foreignXX"), vec![]);
2004 let err = s.compose_generation(&spec).await.unwrap_err();
2005 assert!(matches!(err, StoreError::PreconditionFailed(_)));
2006 assert!(
2007 s.list_uploads("f").await.unwrap().is_empty(),
2008 "a failed compose must abort its MPU (A9)"
2009 );
2010 }
2011 #[tokio::test]
2016 async fn compose_without_a_claimed_checksum_publishes_the_content_crc() {
2017 let s = MemoryStore::new();
2018 let dir = tempfile::TempDir::new().unwrap();
2019 let local = dir.path().join("g.local");
2020 let content: Vec<u8> = (0..24u8).collect();
2021 std::fs::write(&local, &content).unwrap();
2022 let spec = ComposeSpec {
2023 progress: None,
2024 key: "g",
2025 local: Some(std::sync::Arc::new(std::fs::File::open(&local).unwrap())),
2026 parts: vec![
2027 PartSource::Local { offset: 0, len: 8 },
2028 PartSource::Local { offset: 8, len: 8 },
2029 PartSource::Local { offset: 16, len: 8 },
2030 ],
2031 base_key: None,
2032 base_etag: None,
2033 condition: PutCondition::IfNoneMatchAny,
2034 stamps: stamps(1),
2035 crc64: None,
2036 };
2037 let m = s.compose_generation(&spec).await.unwrap();
2038 assert_eq!(m.crc64_b64.as_deref(), Some(crc64_to_b64(crc64_nvme(&content)).as_str()));
2039 let (h, bytes) = s.get_whole("g", None).await.unwrap();
2040 assert_eq!(&bytes[..], &content[..]);
2041 assert_eq!(h.crc64_b64, m.crc64_b64);
2042
2043 let base_body = Bytes::from_static(b"AAAABBBB");
2044 let base = s
2045 .put_whole("h", base_body.clone(), &PutCondition::IfNoneMatchAny, &stamps(1), crc64_nvme(&base_body))
2046 .await
2047 .unwrap();
2048 std::fs::write(&local, b"XXXXBBBB").unwrap();
2049 let err = s
2050 .compose_generation(&ComposeSpec {
2051 progress: None,
2052 key: "h",
2053 local: Some(std::sync::Arc::new(std::fs::File::open(&local).unwrap())),
2054 parts: vec![
2055 PartSource::Local { offset: 0, len: 4 },
2056 PartSource::BaseCopy { offset: 4, len: 4 },
2057 ],
2058 base_key: None,
2059 base_etag: Some(base.etag.clone()),
2060 condition: PutCondition::IfMatch(base.etag.clone()),
2061 stamps: stamps(2),
2062 crc64: None,
2063 })
2064 .await
2065 .unwrap_err();
2066 assert!(matches!(err, StoreError::Other(ref m) if m.contains("copied part")), "{err:?}");
2067 assert!(s.list_uploads("h").await.unwrap().is_empty(), "nothing pending after a refusal");
2068 }
2069}
2070
2071#[cfg(test)]
2072mod copy_tests {
2073 use super::*;
2074 use crate::{crc64_from_b64, crc64_to_b64};
2075
2076 fn stamps(g: u64) -> GenerationStamps {
2077 GenerationStamps {
2078 generation: g,
2079 epoch: 7,
2080 flush_uuid: format!("flush-{g}"),
2081 boundary_source: None,
2082 posix: None,
2083 }
2084 }
2085
2086 #[tokio::test]
2087 async fn a_copy_carries_the_bytes_and_the_checksum_but_not_the_stamps() {
2088 let s = MemoryStore::new();
2089 let body = Bytes::from_static(b"the bytes that move");
2090 let src = s
2091 .put_whole("a.txt", body.clone(), &PutCondition::IfNoneMatchAny, &stamps(3), crc64_nvme(&body))
2092 .await
2093 .unwrap();
2094
2095 let dst = s
2096 .copy_object("a.txt", Some(&src.etag), "b.txt", &PutCondition::IfNoneMatchAny, &stamps(1))
2097 .await
2098 .unwrap();
2099
2100 let (meta, got) = s.get_whole("b.txt", None).await.unwrap();
2101 assert_eq!(got, body, "a copy is byte-identical or it is not a copy");
2102 assert_eq!(
2103 dst.crc64_b64, src.crc64_b64,
2104 "identical bytes have an identical checksum — a copy cannot change one"
2105 );
2106 assert_eq!(
2109 meta.meta.get("flint-gen").map(String::as_str),
2110 Some("1"),
2111 "the destination's generation is its OWN, not the source's 3: {:?}",
2112 meta.meta
2113 );
2114 assert_eq!(
2115 meta.meta.get(GenerationStamps::META_FLUSH_UUID).map(String::as_str),
2116 Some("flush-1"),
2117 "and its flush_uuid names the publish that WROTE IT, not flush-3: {:?}",
2118 meta.meta
2119 );
2120 assert!(s.head("a.txt").await.is_ok());
2122 }
2123
2124 #[tokio::test]
2125 async fn a_copy_refuses_when_the_source_moved_under_it() {
2126 let s = MemoryStore::new();
2127 let v1 = Bytes::from_static(b"version one");
2128 let src = s
2129 .put_whole("a.txt", v1, &PutCondition::IfNoneMatchAny, &stamps(1), crc64_nvme(b"version one"))
2130 .await
2131 .unwrap();
2132 let v2 = Bytes::from_static(b"version two");
2133 s.put_whole("a.txt", v2, &PutCondition::IfMatch(src.etag.clone()), &stamps(2), crc64_nvme(b"version two"))
2134 .await
2135 .unwrap();
2136
2137 let err = s
2138 .copy_object("a.txt", Some(&src.etag), "b.txt", &PutCondition::IfNoneMatchAny, &stamps(1))
2139 .await
2140 .expect_err("copying an object that moved copies something the caller never read");
2141 assert!(matches!(err, StoreError::PreconditionFailed(_)), "{err:?}");
2142 assert!(matches!(s.head("b.txt").await, Err(StoreError::NotFound(_))), "and nothing landed");
2143 }
2144
2145 #[tokio::test]
2146 async fn a_copy_honours_the_destination_precondition() {
2147 let s = MemoryStore::new();
2148 let a = Bytes::from_static(b"aaaa");
2149 s.put_whole("a.txt", a, &PutCondition::IfNoneMatchAny, &stamps(1), crc64_nvme(b"aaaa"))
2150 .await
2151 .unwrap();
2152 let b = Bytes::from_static(b"bbbb");
2153 let existing = s
2154 .put_whole("b.txt", b.clone(), &PutCondition::IfNoneMatchAny, &stamps(1), crc64_nvme(b"bbbb"))
2155 .await
2156 .unwrap();
2157
2158 let err = s
2159 .copy_object("a.txt", None, "b.txt", &PutCondition::IfNoneMatchAny, &stamps(2))
2160 .await
2161 .expect_err("a rename must not silently clobber its destination");
2162 assert!(matches!(err, StoreError::PreconditionFailed(_)), "{err:?}");
2163 let (_, still) = s.get_whole("b.txt", Some(&existing.etag)).await.unwrap();
2164 assert_eq!(still, b, "the destination is untouched by a refused copy");
2165 }
2166
2167 #[tokio::test]
2172 async fn a_compose_can_copy_ranges_from_a_different_key() {
2173 let s = MemoryStore::new();
2174 let body = Bytes::from_static(b"AAAABBBB");
2175 let src = s
2176 .put_whole("old/name", body.clone(), &PutCondition::IfNoneMatchAny, &stamps(1), crc64_nvme(&body))
2177 .await
2178 .unwrap();
2179
2180 let spec = ComposeSpec {
2181 progress: None,
2182 key: "new/name",
2183 local: None,
2185 parts: vec![PartSource::BaseCopy { offset: 0, len: 8 }],
2186 base_key: Some("old/name"),
2187 base_etag: Some(src.etag.clone()),
2188 condition: PutCondition::IfNoneMatchAny,
2189 stamps: stamps(1),
2190 crc64: Some(crc64_nvme(&body)),
2191 };
2192 let m = s.compose_generation(&spec).await.unwrap();
2193 let (_, got) = s.get_whole("new/name", Some(&m.etag)).await.unwrap();
2194 assert_eq!(got, body, "the ranges came from the OTHER key");
2195 assert!(s.head("old/name").await.is_ok(), "and the source survives");
2196 }
2197
2198 #[test]
2199 fn crc64_b64_round_trips_and_refuses_what_is_not_one() {
2200 for v in [0u64, 1, u64::MAX, 0x0123_4567_89ab_cdef] {
2201 assert_eq!(crc64_from_b64(&crc64_to_b64(v)), Some(v), "round trip {v:#x}");
2202 }
2203 for bad in ["", "AAAA", "not-base64!!", "AAAAAAAAAAAA", "AAAAAAAAAAA", "AAAAAAAAAAAAAAA="] {
2212 assert_eq!(crc64_from_b64(bad), None, "{bad:?} is not a canonical CRC-64");
2213 }
2214 assert_eq!(crc64_from_b64("AAAAAAAAAAA="), Some(0), "the canonical form IS accepted");
2215 }
2216}