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