1use std::collections::HashMap;
9use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
10use std::sync::Arc;
11use std::sync::Mutex;
12
13use async_trait::async_trait;
14use bytes::Bytes;
15
16use super::{
17 BlobBody, BoxPartSink, CloudAccessOutcome, CloudAccessState, CloudFileReadError, CloudHome,
18 CloudHomeError, ExactSlotStorage, ObjectSlot, PartSink, UploadProgress,
19};
20
21#[derive(Clone)]
22struct AppendPause {
23 call: usize,
24 reached: Arc<tokio::sync::Notify>,
25 release: Arc<tokio::sync::Notify>,
26}
27
28struct ExactStreamReadGuard {
29 inflight: Arc<AtomicUsize>,
30}
31
32#[derive(Clone)]
33struct ProbePause {
34 reached: Arc<tokio::sync::Notify>,
35 release: Arc<tokio::sync::Notify>,
36}
37
38impl Drop for ExactStreamReadGuard {
39 fn drop(&mut self) {
40 self.inflight.fetch_sub(1, Ordering::SeqCst);
41 }
42}
43
44#[derive(Clone)]
56pub struct InMemoryCloudHome {
57 provider_binding: crate::sync::storage::ResolvedProviderBinding,
58 writes: Arc<Mutex<HashMap<String, Vec<u8>>>>,
59 exact_slot_allocations: Arc<AtomicUsize>,
60 deletes: Arc<Mutex<Vec<String>>>,
61 fail_writes: Arc<AtomicBool>,
62 fail_next_range_reads: Arc<AtomicUsize>,
63 sort_listings: Arc<AtomicBool>,
64 exact_create_count: Arc<AtomicUsize>,
65 fail_exact_create_before: Arc<AtomicUsize>,
66 fail_exact_create_after: Arc<AtomicUsize>,
67 corrupt_exact_readback: Arc<AtomicUsize>,
68 exact_create_pause: Arc<Mutex<Option<AppendPause>>>,
69 probe_pause: Arc<Mutex<Option<ProbePause>>>,
70 exact_full_read_count: Arc<AtomicUsize>,
71 exact_stream_read_count: Arc<AtomicUsize>,
72 exact_reads: Arc<Mutex<Vec<ObjectSlot>>>,
73 exact_stream_read_inflight: Arc<AtomicUsize>,
74 exact_stream_read_max_inflight: Arc<AtomicUsize>,
75 exact_stream_read_barrier: Arc<Mutex<Option<Arc<tokio::sync::Barrier>>>>,
76 exact_delete_count: Arc<AtomicUsize>,
77 fail_exact_delete_on: Arc<AtomicUsize>,
78 fail_exact_delete_of: Arc<Mutex<Option<TargetedDeleteFailure>>>,
79}
80
81struct TargetedDeleteFailure {
84 keys: std::collections::HashSet<String>,
85 countdown: usize,
86}
87
88impl InMemoryCloudHome {
89 pub fn new() -> Self {
90 Self {
91 provider_binding: crate::sync::storage::ResolvedProviderBinding {
92 store: crate::sync::storage::StoreProviderBinding::S3 {
93 endpoint: crate::sync::storage::S3EndpointBinding::Custom {
94 origin: "https://in-memory.invalid".to_string(),
95 },
96 region: "test".to_string(),
97 bucket: "in-memory".to_string(),
98 key_prefix: None,
99 },
100 device: crate::sync::storage::ProviderDeviceBinding {
101 principal: crate::sync::storage::ProviderPrincipalId::CustomS3Credential {
102 access_key_id_hash: crate::sync::store_commit::ObjectHash::digest(
103 b"coven.s3-access-key-id.v1\0in-memory",
104 ),
105 },
106 },
107 },
108 writes: Arc::new(Mutex::new(HashMap::new())),
109 exact_slot_allocations: Arc::new(AtomicUsize::new(0)),
110 deletes: Arc::new(Mutex::new(Vec::new())),
111 fail_writes: Arc::new(AtomicBool::new(false)),
112 fail_next_range_reads: Arc::new(AtomicUsize::new(0)),
113 sort_listings: Arc::new(AtomicBool::new(false)),
114 exact_create_count: Arc::new(AtomicUsize::new(0)),
115 fail_exact_create_before: Arc::new(AtomicUsize::new(0)),
116 fail_exact_create_after: Arc::new(AtomicUsize::new(0)),
117 corrupt_exact_readback: Arc::new(AtomicUsize::new(0)),
118 exact_create_pause: Arc::new(Mutex::new(None)),
119 probe_pause: Arc::new(Mutex::new(None)),
120 exact_full_read_count: Arc::new(AtomicUsize::new(0)),
121 exact_stream_read_count: Arc::new(AtomicUsize::new(0)),
122 exact_reads: Arc::new(Mutex::new(Vec::new())),
123 exact_stream_read_inflight: Arc::new(AtomicUsize::new(0)),
124 exact_stream_read_max_inflight: Arc::new(AtomicUsize::new(0)),
125 exact_stream_read_barrier: Arc::new(Mutex::new(None)),
126 exact_delete_count: Arc::new(AtomicUsize::new(0)),
127 fail_exact_delete_on: Arc::new(AtomicUsize::new(0)),
128 fail_exact_delete_of: Arc::new(Mutex::new(None)),
129 }
130 }
131
132 pub fn with_provider_binding(
133 mut self,
134 binding: crate::sync::storage::ResolvedProviderBinding,
135 ) -> Self {
136 binding
137 .validate()
138 .expect("in-memory provider binding must be valid");
139 self.provider_binding = binding;
140 self
141 }
142
143 pub fn sort_listings(&self) {
149 self.sort_listings.store(true, Ordering::SeqCst);
150 }
151
152 pub fn arm_write_failures(&self) {
157 self.fail_writes.store(true, Ordering::SeqCst);
158 }
159
160 pub fn fail_next_range_reads(&self, n: usize) {
165 self.fail_next_range_reads.store(n, Ordering::SeqCst);
166 }
167
168 pub fn fail_exact_create_before_call(&self, call: usize) {
170 assert!(call > 0, "create call numbers are 1-based");
171 self.exact_create_count.store(0, Ordering::SeqCst);
172 self.fail_exact_create_before.store(call, Ordering::SeqCst);
173 }
174
175 pub fn fail_exact_create_after_call(&self, call: usize) {
177 assert!(call > 0, "create call numbers are 1-based");
178 self.exact_create_count.store(0, Ordering::SeqCst);
179 self.fail_exact_create_after.store(call, Ordering::SeqCst);
180 }
181
182 pub fn corrupt_exact_readback_on_call(&self, call: usize) {
184 assert!(call > 0, "create call numbers are 1-based");
185 self.exact_create_count.store(0, Ordering::SeqCst);
186 self.corrupt_exact_readback.store(call, Ordering::SeqCst);
187 }
188
189 pub fn pause_after_exact_create_call(
191 &self,
192 call: usize,
193 ) -> (Arc<tokio::sync::Notify>, Arc<tokio::sync::Notify>) {
194 assert!(call > 0, "create call numbers are 1-based");
195 self.exact_create_count.store(0, Ordering::SeqCst);
196 let reached = Arc::new(tokio::sync::Notify::new());
197 let release = Arc::new(tokio::sync::Notify::new());
198 *self.exact_create_pause.lock().unwrap() = Some(AppendPause {
199 call,
200 reached: reached.clone(),
201 release: release.clone(),
202 });
203 (reached, release)
204 }
205
206 pub fn pause_next_probe(&self) -> (Arc<tokio::sync::Notify>, Arc<tokio::sync::Notify>) {
208 let reached = Arc::new(tokio::sync::Notify::new());
209 let release = Arc::new(tokio::sync::Notify::new());
210 *self.probe_pause.lock().unwrap() = Some(ProbePause {
211 reached: reached.clone(),
212 release: release.clone(),
213 });
214 (reached, release)
215 }
216
217 pub fn exact_create_count(&self) -> usize {
218 self.exact_create_count.load(Ordering::SeqCst)
219 }
220
221 pub fn exact_full_read_count(&self) -> usize {
222 self.exact_full_read_count.load(Ordering::SeqCst)
223 }
224
225 pub fn exact_stream_read_count(&self) -> usize {
226 self.exact_stream_read_count.load(Ordering::SeqCst)
227 }
228
229 pub fn exact_reads(&self) -> Vec<ObjectSlot> {
230 self.exact_reads.lock().unwrap().clone()
231 }
232
233 pub fn clear_exact_reads(&self) {
234 self.exact_reads.lock().unwrap().clear();
235 }
236
237 pub fn arm_exact_stream_read_concurrency_probe(&self, width: usize) {
238 assert!(width > 0, "exact stream read probe width must be positive");
239 self.exact_stream_read_inflight.store(0, Ordering::SeqCst);
240 self.exact_stream_read_max_inflight
241 .store(0, Ordering::SeqCst);
242 *self.exact_stream_read_barrier.lock().unwrap() =
243 Some(Arc::new(tokio::sync::Barrier::new(width)));
244 }
245
246 pub fn exact_stream_read_max_inflight(&self) -> usize {
247 self.exact_stream_read_max_inflight.load(Ordering::SeqCst)
248 }
249
250 pub fn exact_delete_count(&self) -> usize {
251 self.exact_delete_count.load(Ordering::SeqCst)
252 }
253
254 pub fn fail_exact_delete_on_call(&self, call: usize) {
255 assert!(call > 0, "exact-delete call numbers are 1-based");
256 self.exact_delete_count.store(0, Ordering::SeqCst);
257 self.fail_exact_delete_on.store(call, Ordering::SeqCst);
258 }
259
260 pub fn fail_nth_exact_delete_of(&self, slots: &[&ObjectSlot], nth: usize) {
267 assert!(nth > 0, "targeted delete ordinals are 1-based");
268 let keys = slots
269 .iter()
270 .map(|slot| Self::exact_storage_key(slot).expect("test exact slot is valid"))
271 .collect();
272 *self.fail_exact_delete_of.lock().unwrap() = Some(TargetedDeleteFailure {
273 keys,
274 countdown: nth,
275 });
276 }
277
278 pub fn remove(&self, key: &str) {
282 self.writes.lock().unwrap().remove(key);
283 }
284
285 pub fn keys(&self) -> Vec<String> {
288 self.writes.lock().unwrap().keys().cloned().collect()
289 }
290
291 pub fn get(&self, key: &str) -> Option<Vec<u8>> {
295 self.writes.lock().unwrap().get(key).cloned()
296 }
297
298 pub fn len(&self) -> usize {
300 self.writes.lock().unwrap().len()
301 }
302
303 pub fn is_empty(&self) -> bool {
305 self.writes.lock().unwrap().is_empty()
306 }
307
308 pub fn deletes_seen(&self) -> Vec<String> {
310 self.deletes.lock().unwrap().clone()
311 }
312
313 pub fn insert_exact_object(&self, logical_key: &str, bytes: Vec<u8>) -> ObjectSlot {
315 let slot =
316 ObjectSlot::logical(logical_key.to_string()).expect("test logical key is non-empty");
317 self.writes
318 .lock()
319 .unwrap()
320 .insert(logical_key.to_string(), bytes);
321 slot
322 }
323
324 pub fn stored_exact_bytes(&self, slot: &ObjectSlot) -> Option<Vec<u8>> {
326 let key = Self::exact_storage_key(slot).expect("test exact slot is valid");
327 self.writes.lock().unwrap().get(&key).cloned()
328 }
329
330 pub fn restore_exact_object(&self, slot: &ObjectSlot, bytes: Vec<u8>) {
333 let key = Self::exact_storage_key(slot).expect("test exact slot is valid");
334 self.writes.lock().unwrap().insert(key, bytes);
335 }
336
337 pub fn remove_exact_object(&self, slot: &ObjectSlot) {
339 let key = Self::exact_storage_key(slot).expect("test exact slot is valid");
340 self.writes.lock().unwrap().remove(&key);
341 }
342
343 pub fn replace_exact_object(&self, slot: &ObjectSlot, bytes: Vec<u8>) {
345 let previous = self.writes.lock().unwrap().insert(
346 Self::exact_storage_key(slot).expect("test exact slot is valid"),
347 bytes,
348 );
349 assert!(previous.is_some(), "exact slot exists");
350 }
351}
352
353impl Default for InMemoryCloudHome {
354 fn default() -> Self {
355 Self::new()
356 }
357}
358
359struct InMemoryPartSink {
363 writes: Arc<Mutex<HashMap<String, Vec<u8>>>>,
364 key: String,
365 buf: Vec<u8>,
366}
367
368#[async_trait]
369impl PartSink for InMemoryPartSink {
370 fn part_size(&self) -> usize {
371 super::PROGRESS_CHUNK_SIZE
372 }
373
374 async fn send_part(
375 &mut self,
376 part: Bytes,
377 _offset: u64,
378 _is_last: bool,
379 ) -> Result<(), CloudHomeError> {
380 self.buf.extend_from_slice(&part);
381 Ok(())
382 }
383
384 async fn abort(&mut self) -> Result<(), CloudHomeError> {
385 Ok(())
386 }
387
388 async fn finish(self: Box<Self>) -> Result<(), CloudHomeError> {
389 self.writes.lock().unwrap().insert(self.key, self.buf);
390 Ok(())
391 }
392}
393
394impl InMemoryCloudHome {
395 async fn put_object(&self, key: &str, data: Vec<u8>) -> Result<(), CloudHomeError> {
396 if self.fail_writes.load(Ordering::SeqCst) {
397 return Err(CloudHomeError::Transport(
398 "InMemoryCloudHome: armed write failure".into(),
399 ));
400 }
401 self.writes.lock().unwrap().insert(key.to_string(), data);
402 Ok(())
403 }
404
405 async fn open_multipart<'a>(
406 &'a self,
407 key: &str,
408 _total_len: u64,
409 ) -> Result<BoxPartSink<'a>, CloudHomeError> {
410 if self.fail_writes.load(Ordering::SeqCst) {
413 return Err(CloudHomeError::Transport(
414 "InMemoryCloudHome: armed write failure".into(),
415 ));
416 }
417 Ok(Box::new(InMemoryPartSink {
418 writes: self.writes.clone(),
419 key: key.to_string(),
420 buf: Vec::new(),
421 }))
422 }
423
424 fn multipart_threshold(&self) -> u64 {
425 super::PROGRESS_CHUNK_SIZE as u64
428 }
429 fn validate_exact_slot(slot: &ObjectSlot) -> Result<(), CloudHomeError> {
430 slot.validate()?;
431 Ok(())
432 }
433
434 fn exact_storage_key(slot: &ObjectSlot) -> Result<String, CloudHomeError> {
435 Self::validate_exact_slot(slot)?;
436 Ok(match slot.physical() {
437 super::PhysicalObjectLocator::LogicalKey => slot.logical_key().to_string(),
438 super::PhysicalObjectLocator::Opaque(provider_id) => {
439 format!("{}#exact#{provider_id}", slot.logical_key())
440 }
441 })
442 }
443
444 async fn create_at_slot(
445 &self,
446 slot: &ObjectSlot,
447 body: BlobBody,
448 progress: &UploadProgress<'_>,
449 ) -> Result<(), CloudHomeError> {
450 if self.fail_writes.load(Ordering::SeqCst) {
451 return Err(CloudHomeError::Transport(
452 "InMemoryCloudHome: armed write failure".into(),
453 ));
454 }
455 let key = Self::exact_storage_key(slot)?;
456 let call = self.exact_create_count.fetch_add(1, Ordering::SeqCst) + 1;
457 if self.fail_exact_create_before.load(Ordering::SeqCst) == call {
458 self.fail_exact_create_before.store(0, Ordering::SeqCst);
459 return Err(CloudHomeError::Transport(format!(
460 "InMemoryCloudHome: forced failure before exact create call {call}"
461 )));
462 }
463 let bytes = body.collect().await?;
464 progress(bytes.len() as u64);
465 {
466 let mut writes = self.writes.lock().unwrap();
467 if writes.contains_key(&key) {
468 return Err(CloudHomeError::AlreadyExists(key));
469 }
470 writes.insert(key.clone(), bytes);
471 }
472 if self.corrupt_exact_readback.load(Ordering::SeqCst) == call {
473 self.corrupt_exact_readback.store(0, Ordering::SeqCst);
474 self.writes
475 .lock()
476 .unwrap()
477 .insert(key.clone(), b"corrupt readback".to_vec());
478 }
479 let pause = self
480 .exact_create_pause
481 .lock()
482 .unwrap()
483 .clone()
484 .filter(|pause| pause.call == call);
485 if let Some(pause) = pause {
486 pause.reached.notify_one();
487 pause.release.notified().await;
488 self.exact_create_pause.lock().unwrap().take();
489 }
490 if self.fail_exact_create_after.load(Ordering::SeqCst) == call {
491 self.fail_exact_create_after.store(0, Ordering::SeqCst);
492 return Err(CloudHomeError::Transport(format!(
493 "InMemoryCloudHome: forced failure after exact create call {call}"
494 )));
495 }
496 Ok(())
497 }
498
499 async fn read_exact(&self, slot: &ObjectSlot) -> Result<Vec<u8>, CloudHomeError> {
500 self.exact_full_read_count.fetch_add(1, Ordering::SeqCst);
501 let key = Self::exact_storage_key(slot)?;
502 self.exact_reads.lock().unwrap().push(slot.clone());
503 self.writes
504 .lock()
505 .unwrap()
506 .get(&key)
507 .cloned()
508 .ok_or_else(|| CloudHomeError::NotFound(slot.logical_key().to_string()))
509 }
510
511 async fn read_exact_to_file(
512 &self,
513 slot: &ObjectSlot,
514 destination: &std::path::Path,
515 ) -> Result<(), CloudFileReadError> {
516 self.exact_stream_read_count.fetch_add(1, Ordering::SeqCst);
517 self.exact_reads.lock().unwrap().push(slot.clone());
518 let inflight = self
519 .exact_stream_read_inflight
520 .fetch_add(1, Ordering::SeqCst)
521 + 1;
522 self.exact_stream_read_max_inflight
523 .fetch_max(inflight, Ordering::SeqCst);
524 let _guard = ExactStreamReadGuard {
525 inflight: self.exact_stream_read_inflight.clone(),
526 };
527 let barrier = self.exact_stream_read_barrier.lock().unwrap().clone();
528 if let Some(barrier) = barrier {
529 barrier.wait().await;
530 }
531 let key = Self::exact_storage_key(slot)?;
532 let bytes = self
533 .writes
534 .lock()
535 .unwrap()
536 .get(&key)
537 .cloned()
538 .ok_or_else(|| CloudHomeError::NotFound(slot.logical_key().to_string()))?;
539 let stream = futures_util::stream::once(async move { Ok(bytes::Bytes::from(bytes)) });
540 super::write_cloud_object_stream(destination, Box::pin(stream)).await?;
541 Ok(())
542 }
543
544 async fn delete_exact(&self, slot: &ObjectSlot) -> Result<(), CloudHomeError> {
545 let key = Self::exact_storage_key(slot)?;
546 let call = self.exact_delete_count.fetch_add(1, Ordering::SeqCst) + 1;
547 if self.fail_exact_delete_on.load(Ordering::SeqCst) == call {
548 self.fail_exact_delete_on.store(0, Ordering::SeqCst);
549 return Err(CloudHomeError::Transport(format!(
550 "InMemoryCloudHome: forced exact delete failure on call {call}"
551 )));
552 }
553 {
554 let mut targeted = self.fail_exact_delete_of.lock().unwrap();
555 if let Some(failure) = targeted.as_mut() {
556 if failure.keys.contains(&key) {
557 failure.countdown -= 1;
558 if failure.countdown == 0 {
559 *targeted = None;
560 return Err(CloudHomeError::Transport(format!(
561 "InMemoryCloudHome: forced exact delete failure of {key}"
562 )));
563 }
564 }
565 }
566 }
567 self.writes.lock().unwrap().remove(&key);
568 self.deletes.lock().unwrap().push(key);
569 Ok(())
570 }
571 async fn read(&self, key: &str) -> Result<Vec<u8>, CloudHomeError> {
572 self.writes
573 .lock()
574 .unwrap()
575 .get(key)
576 .cloned()
577 .ok_or_else(|| CloudHomeError::NotFound(key.to_string()))
578 }
579
580 async fn read_range(&self, key: &str, start: u64, end: u64) -> Result<Vec<u8>, CloudHomeError> {
581 if self
585 .fail_next_range_reads
586 .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |n| n.checked_sub(1))
587 .is_ok()
588 {
589 return Err(CloudHomeError::Transport(
590 "InMemoryCloudHome: armed range-read failure".into(),
591 ));
592 }
593 let data = self.read(key).await?;
594 let s = start as usize;
595 let e = (end as usize).min(data.len());
596 if s > data.len() {
597 return Err(CloudHomeError::NotFound(format!("range past end of {key}")));
598 }
599 Ok(data[s..e].to_vec())
600 }
601
602 async fn list(&self, prefix: &str) -> Result<Vec<String>, CloudHomeError> {
603 let mut keys: Vec<String> = self
604 .writes
605 .lock()
606 .unwrap()
607 .keys()
608 .filter(|k| k.starts_with(prefix))
609 .cloned()
610 .collect();
611 if self.sort_listings.load(Ordering::SeqCst) {
612 keys.sort();
613 }
614 Ok(keys)
615 }
616
617 async fn delete(&self, key: &str) -> Result<(), CloudHomeError> {
618 self.writes.lock().unwrap().remove(key);
619 self.deletes.lock().unwrap().push(key.to_string());
620 Ok(())
621 }
622
623 async fn exists(&self, key: &str) -> Result<bool, CloudHomeError> {
624 Ok(self.writes.lock().unwrap().contains_key(key))
625 }
626
627 async fn set_access(
628 &self,
629 desired: super::CloudAccessState,
630 ) -> Result<super::CloudAccessOutcome, CloudHomeError> {
631 Ok(match desired {
632 super::CloudAccessState::Present { .. } => {
633 super::CloudAccessOutcome::Present(super::CloudHomeJoinInfo::S3 {
634 bucket: "in-memory".to_string(),
635 region: "test".to_string(),
636 endpoint: Some("https://in-memory.invalid".to_string()),
637 access_key: "in-memory".to_string(),
638 secret_key: "in-memory".to_string(),
639 key_prefix: None,
640 })
641 }
642 super::CloudAccessState::Absent { .. } => {
643 super::CloudAccessOutcome::Absent(super::RevokeOutcome::Unsupported)
644 }
645 })
646 }
647}
648
649#[async_trait]
650impl CloudHome for InMemoryCloudHome {
651 fn exact_slot_storage(self: Arc<Self>) -> Option<Arc<dyn ExactSlotStorage>> {
652 Some(self)
653 }
654
655 async fn probe(&self) -> Result<(), CloudHomeError> {
656 let pause = self.probe_pause.lock().unwrap().take();
657 if let Some(pause) = pause {
658 pause.reached.notify_one();
659 pause.release.notified().await;
660 }
661 Ok(())
662 }
663
664 async fn put_object(&self, key: &str, data: Vec<u8>) -> Result<(), CloudHomeError> {
665 InMemoryCloudHome::put_object(self, key, data).await
666 }
667
668 async fn open_multipart<'a>(
669 &'a self,
670 key: &str,
671 total_len: u64,
672 ) -> Result<BoxPartSink<'a>, CloudHomeError> {
673 InMemoryCloudHome::open_multipart(self, key, total_len).await
674 }
675
676 fn multipart_threshold(&self) -> u64 {
677 InMemoryCloudHome::multipart_threshold(self)
678 }
679
680 async fn read(&self, key: &str) -> Result<Vec<u8>, CloudHomeError> {
681 InMemoryCloudHome::read(self, key).await
682 }
683
684 async fn read_range(&self, key: &str, start: u64, end: u64) -> Result<Vec<u8>, CloudHomeError> {
685 InMemoryCloudHome::read_range(self, key, start, end).await
686 }
687
688 async fn list(&self, prefix: &str) -> Result<Vec<String>, CloudHomeError> {
689 InMemoryCloudHome::list(self, prefix).await
690 }
691
692 async fn delete(&self, key: &str) -> Result<(), CloudHomeError> {
693 InMemoryCloudHome::delete(self, key).await
694 }
695
696 async fn exists(&self, key: &str) -> Result<bool, CloudHomeError> {
697 InMemoryCloudHome::exists(self, key).await
698 }
699
700 async fn set_access(
701 &self,
702 desired: CloudAccessState,
703 ) -> Result<CloudAccessOutcome, CloudHomeError> {
704 InMemoryCloudHome::set_access(self, desired).await
705 }
706}
707
708#[async_trait]
709impl ExactSlotStorage for InMemoryCloudHome {
710 async fn provider_binding(
711 &self,
712 ) -> Result<crate::sync::storage::ResolvedProviderBinding, CloudHomeError> {
713 Ok(self.provider_binding.clone())
714 }
715
716 async fn allocate_slot(&self, logical_key: &str) -> Result<ObjectSlot, CloudHomeError> {
717 match &self.provider_binding.store {
718 crate::sync::storage::StoreProviderBinding::GoogleDrive { .. } => {
719 let allocation = self.exact_slot_allocations.fetch_add(1, Ordering::SeqCst) + 1;
720 ObjectSlot::opaque(logical_key.to_string(), format!("in-memory-{allocation}"))
721 }
722 crate::sync::storage::StoreProviderBinding::S3 { .. }
723 | crate::sync::storage::StoreProviderBinding::Dropbox { .. }
724 | crate::sync::storage::StoreProviderBinding::OneDrive { .. }
725 | crate::sync::storage::StoreProviderBinding::CloudKit { .. } => {
726 ObjectSlot::logical(logical_key.to_string())
727 }
728 }
729 }
730
731 async fn create_at(
732 &self,
733 slot: &ObjectSlot,
734 body: BlobBody,
735 progress: &UploadProgress<'_>,
736 ) -> Result<(), CloudHomeError> {
737 InMemoryCloudHome::create_at_slot(self, slot, body, progress).await
738 }
739
740 async fn read_at(&self, slot: &ObjectSlot) -> Result<Vec<u8>, CloudHomeError> {
741 InMemoryCloudHome::read_exact(self, slot).await
742 }
743
744 async fn read_range_at(
745 &self,
746 slot: &ObjectSlot,
747 start: u64,
748 end: u64,
749 ) -> Result<Vec<u8>, CloudHomeError> {
750 let bytes = InMemoryCloudHome::read_exact(self, slot).await?;
751 let start = start as usize;
752 let end = (end as usize).min(bytes.len());
753 if start > bytes.len() {
754 return Err(CloudHomeError::NotFound(format!(
755 "range past end of {}",
756 slot.logical_key()
757 )));
758 }
759 Ok(bytes[start..end].to_vec())
760 }
761
762 async fn read_at_to_file(
763 &self,
764 slot: &ObjectSlot,
765 destination: &std::path::Path,
766 ) -> Result<(), CloudFileReadError> {
767 InMemoryCloudHome::read_exact_to_file(self, slot, destination).await
768 }
769
770 async fn delete_at(&self, slot: &ObjectSlot) -> Result<(), CloudHomeError> {
771 InMemoryCloudHome::delete_exact(self, slot).await
772 }
773}
774
775#[cfg(test)]
776mod tests {
777 use super::*;
778 use crate::storage::cloud::{no_progress, BlobBody};
779 use std::sync::atomic::{AtomicU64, Ordering};
780 use std::sync::Arc;
781
782 #[tokio::test]
783 async fn write_then_read_roundtrips() {
784 let h = InMemoryCloudHome::new();
785 h.write(
786 "foo",
787 BlobBody::from_bytes(b"hello".to_vec()),
788 &no_progress(),
789 )
790 .await
791 .unwrap();
792 assert_eq!(h.read("foo").await.unwrap(), b"hello");
793 assert!(h.exists("foo").await.unwrap());
794 assert!(!h.exists("bar").await.unwrap());
795 }
796
797 #[tokio::test]
798 async fn write_reports_progress_in_chunks_reaching_the_total() {
799 let h = InMemoryCloudHome::new();
800 let len = super::super::PROGRESS_CHUNK_SIZE * 2 + 7;
803 let last = Arc::new(AtomicU64::new(0));
804 let ticks = Arc::new(AtomicU64::new(0));
805 let last2 = last.clone();
806 let ticks2 = ticks.clone();
807 let sink = move |n: u64| {
808 last2.store(n, Ordering::Relaxed);
809 ticks2.fetch_add(1, Ordering::Relaxed);
810 };
811 h.write("big", BlobBody::from_bytes(vec![0u8; len]), &sink)
812 .await
813 .unwrap();
814 assert_eq!(last.load(Ordering::Relaxed), len as u64);
815 assert_eq!(ticks.load(Ordering::Relaxed), 3);
816 }
817
818 #[tokio::test]
819 async fn read_range_returns_a_slice() {
820 let h = InMemoryCloudHome::new();
821 h.write(
822 "k",
823 BlobBody::from_bytes(b"0123456789".to_vec()),
824 &no_progress(),
825 )
826 .await
827 .unwrap();
828 assert_eq!(h.read_range("k", 2, 5).await.unwrap(), b"234");
829 }
830
831 #[tokio::test]
832 async fn list_filters_by_prefix() {
833 let h = InMemoryCloudHome::new();
834 h.write("a/x", BlobBody::from_bytes(vec![1]), &no_progress())
835 .await
836 .unwrap();
837 h.write("a/y", BlobBody::from_bytes(vec![2]), &no_progress())
838 .await
839 .unwrap();
840 h.write("b/x", BlobBody::from_bytes(vec![3]), &no_progress())
841 .await
842 .unwrap();
843 let mut got = h.list("a/").await.unwrap();
844 got.sort();
845 assert_eq!(got, vec!["a/x".to_string(), "a/y".to_string()]);
846 }
847
848 #[tokio::test]
849 async fn delete_removes_and_records() {
850 let h = InMemoryCloudHome::new();
851 h.write("k", BlobBody::from_bytes(vec![1]), &no_progress())
852 .await
853 .unwrap();
854 h.delete("k").await.unwrap();
855 assert!(matches!(
856 h.read("k").await,
857 Err(CloudHomeError::NotFound(_))
858 ));
859 assert_eq!(h.deletes_seen(), vec!["k".to_string()]);
860 }
861
862 #[tokio::test]
863 async fn arm_write_failures_fails_writes_after_arming() {
864 let h = InMemoryCloudHome::new();
865 h.write("before", BlobBody::from_bytes(vec![1]), &no_progress())
867 .await
868 .unwrap();
869
870 h.arm_write_failures();
871 let err = h
872 .write("after", BlobBody::from_bytes(vec![2]), &no_progress())
873 .await
874 .unwrap_err();
875 assert!(matches!(err, CloudHomeError::Transport(_)));
876 assert!(err.is_retryable());
877 assert!(h.get("after").is_none());
879 assert_eq!(h.get("before"), Some(vec![1]));
880 }
881
882 #[tokio::test]
883 async fn fail_next_range_reads_fails_the_next_n_then_recovers() {
884 let h = InMemoryCloudHome::new();
885 h.write(
886 "k",
887 BlobBody::from_bytes(b"0123456789".to_vec()),
888 &no_progress(),
889 )
890 .await
891 .unwrap();
892
893 h.fail_next_range_reads(2);
894 assert!(matches!(
895 h.read_range("k", 0, 4).await,
896 Err(CloudHomeError::Transport(_))
897 ));
898 assert!(matches!(
899 h.read_range("k", 0, 4).await,
900 Err(CloudHomeError::Transport(_))
901 ));
902 assert_eq!(h.read_range("k", 0, 4).await.unwrap(), b"0123");
904 }
905
906 #[tokio::test]
907 async fn remove_drops_a_key_out_of_band() {
908 let h = InMemoryCloudHome::new();
909 h.write("k", BlobBody::from_bytes(vec![1]), &no_progress())
910 .await
911 .unwrap();
912
913 h.remove("k");
914 assert!(matches!(
915 h.read("k").await,
916 Err(CloudHomeError::NotFound(_))
917 ));
918 assert!(h.deletes_seen().is_empty());
920 }
921
922 #[tokio::test]
923 async fn exact_create_is_visible_before_a_lost_response() {
924 let h = InMemoryCloudHome::new();
925 let slot = h.allocate_slot("store-v1/test/one.json").await.unwrap();
926 let (reached, release) = h.pause_after_exact_create_call(1);
927 let writer = h.clone();
928 let writer_slot = slot.clone();
929 let task = tokio::spawn(async move {
930 writer
931 .create_at(
932 &writer_slot,
933 BlobBody::from_bytes(b"first".to_vec()),
934 &no_progress(),
935 )
936 .await
937 });
938
939 reached.notified().await;
940 assert_eq!(h.exact_create_count(), 1);
941 assert_eq!(h.read_at(&slot).await.unwrap(), b"first");
942 release.notify_one();
943 task.await.unwrap().unwrap();
944 }
945
946 #[tokio::test]
947 async fn exact_create_never_overwrites() {
948 let h = InMemoryCloudHome::new();
949 let slot = h.allocate_slot("store-v1/test/one.json").await.unwrap();
950 h.create_at(
951 &slot,
952 BlobBody::from_bytes(b"winner".to_vec()),
953 &no_progress(),
954 )
955 .await
956 .unwrap();
957
958 assert!(matches!(
959 h.create_at(
960 &slot,
961 BlobBody::from_bytes(b"loser".to_vec()),
962 &no_progress(),
963 )
964 .await,
965 Err(CloudHomeError::AlreadyExists(_))
966 ));
967 assert_eq!(h.read_at(&slot).await.unwrap(), b"winner");
968 }
969
970 #[tokio::test]
971 async fn google_drive_exact_slots_with_one_logical_key_remain_independent() {
972 let h = InMemoryCloudHome::new().with_provider_binding(
973 crate::sync::storage::ResolvedProviderBinding {
974 store: crate::sync::storage::StoreProviderBinding::GoogleDrive {
975 corpus: crate::sync::storage::GoogleDriveCorpus::SharedDrive {
976 drive_id: "drive-id".to_string(),
977 folder_id: "folder-id".to_string(),
978 },
979 },
980 device: crate::sync::storage::ProviderDeviceBinding {
981 principal: crate::sync::storage::ProviderPrincipalId::GoogleDrive {
982 permission_id: "permission-id".to_string(),
983 },
984 },
985 },
986 );
987 let first = h.allocate_slot("store-v1/test/one.json").await.unwrap();
988 let second = h.allocate_slot("store-v1/test/one.json").await.unwrap();
989 assert_ne!(first, second);
990 h.create_at(
991 &first,
992 BlobBody::from_bytes(b"first".to_vec()),
993 &no_progress(),
994 )
995 .await
996 .unwrap();
997 h.create_at(
998 &second,
999 BlobBody::from_bytes(b"second".to_vec()),
1000 &no_progress(),
1001 )
1002 .await
1003 .unwrap();
1004 assert_eq!(h.read_at(&first).await.unwrap(), b"first");
1005 assert_eq!(h.read_at(&second).await.unwrap(), b"second");
1006 h.delete_at(&first).await.unwrap();
1007 assert!(matches!(
1008 h.read_at(&first).await,
1009 Err(CloudHomeError::NotFound(_))
1010 ));
1011 assert_eq!(h.read_at(&second).await.unwrap(), b"second");
1012 }
1013
1014 #[tokio::test]
1015 async fn google_drive_exact_slots_with_different_logical_keys_have_distinct_file_ids() {
1016 let h = InMemoryCloudHome::new().with_provider_binding(
1017 crate::sync::storage::ResolvedProviderBinding {
1018 store: crate::sync::storage::StoreProviderBinding::GoogleDrive {
1019 corpus: crate::sync::storage::GoogleDriveCorpus::SharedDrive {
1020 drive_id: "drive-id".to_string(),
1021 folder_id: "folder-id".to_string(),
1022 },
1023 },
1024 device: crate::sync::storage::ProviderDeviceBinding {
1025 principal: crate::sync::storage::ProviderPrincipalId::GoogleDrive {
1026 permission_id: "permission-id".to_string(),
1027 },
1028 },
1029 },
1030 );
1031
1032 let first = h.allocate_slot("store-v1/test/one.json").await.unwrap();
1033 let second = h.allocate_slot("store-v1/test/two.json").await.unwrap();
1034
1035 assert_ne!(first.physical(), second.physical());
1036 }
1037
1038 #[tokio::test]
1039 async fn access_matches_the_in_memory_s3_binding() {
1040 let h = InMemoryCloudHome::new();
1041 let desired = CloudAccessState::Present {
1042 member_pubkey: "member".to_string(),
1043 provider_account_email: None,
1044 };
1045
1046 let first = h.set_access(desired.clone()).await.unwrap();
1047 let second = h.set_access(desired).await.unwrap();
1048 let expected = CloudAccessOutcome::Present(super::super::CloudHomeJoinInfo::S3 {
1049 bucket: "in-memory".to_string(),
1050 region: "test".to_string(),
1051 endpoint: Some("https://in-memory.invalid".to_string()),
1052 access_key: "in-memory".to_string(),
1053 secret_key: "in-memory".to_string(),
1054 key_prefix: None,
1055 });
1056 assert_eq!(first, expected);
1057 assert_eq!(second, expected);
1058 assert_eq!(
1059 h.set_access(CloudAccessState::Absent {
1060 member_pubkey: "member".to_string(),
1061 provider_account_email: None,
1062 })
1063 .await
1064 .unwrap(),
1065 CloudAccessOutcome::Absent(super::super::RevokeOutcome::Unsupported)
1066 );
1067 }
1068}