Skip to main content

coven_core/storage/cloud/
test_utils.rs

1//! In-process CloudHome implementation for tests. Records every write keyed
2//! by cloud_key so tests can read back exactly what landed, and serves reads
3//! from the same map — enough to simulate two devices sharing a cloud bucket.
4//!
5//! Available under `#[cfg(test)]` in coven itself and to downstream crates
6//! that enable the `test-utils` feature.
7
8use 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/// In-memory CloudHome backed by a HashMap. `Clone` shares one backing store, so
46/// clones act as separate devices reading and writing the same cloud bucket, and
47/// a test can keep its own handle for direct at-rest assertions while each device
48/// owns a `Box<dyn CloudHome>` clone.
49///
50/// Beyond the happy path it carries fault-injection knobs
51/// ([`arm_write_failures`](Self::arm_write_failures),
52/// [`fail_next_range_reads`](Self::fail_next_range_reads),
53/// [`remove`](Self::remove)) so a host test can drive upload-failure,
54/// read-retry, and missing-blob paths without a bespoke `CloudHome` impl. The
55/// arming state is shared across clones, like the backing store.
56#[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    /// Return `list` results in sorted key order instead of the backing map's
144    /// arbitrary order. A real bucket LIST has no defined order, so the pull's
145    /// cross-device apply order is arbitrary; a test that needs a fixed order (to
146    /// reproduce an order-dependent bug deterministically) arms this and picks the
147    /// order through its device ids.
148    pub fn sort_listings(&self) {
149        self.sort_listings.store(true, Ordering::SeqCst);
150    }
151
152    /// Arm every subsequent write (`put_object` and `open_multipart`) to fail
153    /// with a retryable transport error. A test can let a home's setup writes
154    /// land and then arm this before driving the path whose uploads must fail;
155    /// it stays armed for the store's lifetime.
156    pub fn arm_write_failures(&self) {
157        self.fail_writes.store(true, Ordering::SeqCst);
158    }
159
160    /// Make the next `n` `read_range` calls fail with a retryable transport
161    /// error before any serves bytes, to exercise a caller's read-retry path.
162    /// Each failed call consumes one; once `n` are spent, ranges serve
163    /// normally.
164    pub fn fail_next_range_reads(&self, n: usize) {
165        self.fail_next_range_reads.store(n, Ordering::SeqCst);
166    }
167
168    /// Reset the exact-create counter and fail before the selected call stores bytes.
169    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    /// Reset the exact-create counter and lose the response after the selected create.
176    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    /// Replace the selected exact object's bytes before its verification read.
183    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    /// Pause after the selected exact create is physically visible.
190    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    /// Pause the next reachability probe after it starts and before it succeeds.
207    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    /// Drop `key`'s bytes out of band — as if the object vanished from the
269    /// bucket on its own, without a `delete` (which `deletes_seen` would
270    /// record). Drives missing-blob read failures.
271    pub fn remove(&self, key: &str) {
272        self.writes.lock().unwrap().remove(key);
273    }
274
275    /// Snapshot of every key currently in the cloud. Useful for assertions
276    /// that don't want to hold the lock across an await.
277    pub fn keys(&self) -> Vec<String> {
278        self.writes.lock().unwrap().keys().cloned().collect()
279    }
280
281    /// Snapshot of the bytes at `key`, or `None` if absent. Cloned so the
282    /// caller can hold the result across `await` points without retaining
283    /// the internal lock.
284    pub fn get(&self, key: &str) -> Option<Vec<u8>> {
285        self.writes.lock().unwrap().get(key).cloned()
286    }
287
288    /// Number of objects stored. Cheap snapshot.
289    pub fn len(&self) -> usize {
290        self.writes.lock().unwrap().len()
291    }
292
293    /// Returns true if the store is empty.
294    pub fn is_empty(&self) -> bool {
295        self.writes.lock().unwrap().is_empty()
296    }
297
298    /// Snapshot of every delete that's been requested, in arrival order.
299    pub fn deletes_seen(&self) -> Vec<String> {
300        self.deletes.lock().unwrap().clone()
301    }
302
303    /// Insert caller-selected bytes at one exact logical slot.
304    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    /// Remove one exact object without recording a protocol delete.
315    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    /// Replace bytes at one exact slot without changing its locator.
321    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
446/// A [`PartSink`] for the in-memory backend: accumulate the streamed parts in
447/// order and store the assembled object on `finish`, so a multipart upload
448/// round-trips exactly like a single `put_object`.
449struct 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        // Gate multipart too, so `arm_write_failures` fails a write whatever its
498        // size — `write_blob` routes blobs above `multipart_threshold` here.
499        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        // A small threshold so tests exercise the multipart driver path; the part
513        // size matches so a multi-part blob ticks progress several times.
514        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        // An armed range read fails before touching the store. `checked_sub`
653        // returns `None` at zero, so `fetch_update` only succeeds (and errors)
654        // while the countdown is positive.
655        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        // Two-and-a-bit chunks so progress fires more than once and the final
877        // value equals the total.
878        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        // Writes land before arming.
942        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        // Nothing was stored for the failed write, and the earlier one survives.
954        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        // The third serves real bytes — the countdown is spent.
979        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        // Out-of-band removal is not a delete, so it leaves no delete record.
995        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}