Skip to main content

commonware_storage/metadata/
storage.rs

1use super::{Config, Error};
2use crate::{Context, SyncCompletion};
3use commonware_codec::{Codec, Copying, FixedSize, ReadExt};
4use commonware_cryptography::{Crc32, crc32};
5use commonware_runtime::{
6    BLOB_PAGE_SIZE, Blob, BufMut, Error as RError, Handle, IoBufMut, ReadOptions, WriteOptions,
7    telemetry::metrics::{Counter, Gauge, GaugeExt, MetricsExt as _},
8};
9use commonware_utils::{Span, Widen, futures::try_join_all};
10use futures::FutureExt as _;
11use std::collections::{BTreeMap, BTreeSet, HashMap};
12use tracing::{debug, warn};
13
14/// The names of the two blobs that store metadata.
15const BLOB_NAMES: [&[u8]; 2] = [b"left", b"right"];
16
17/// Information about a value in a [Wrapper].
18struct Info {
19    start: usize,
20    length: usize,
21}
22
23impl Info {
24    /// Create a new [Info].
25    const fn new(start: usize, length: usize) -> Self {
26        Self { start, length }
27    }
28}
29
30/// One of the two wrappers that store metadata.
31struct Wrapper<B: Blob, K: Span> {
32    blob: B,
33    version: u64,
34    lengths: HashMap<K, Info>,
35    modified: BTreeSet<K>,
36    data: IoBufMut,
37}
38
39impl<B: Blob, K: Span> Wrapper<B, K> {
40    /// Create a new [Wrapper].
41    const fn new(blob: B, version: u64, lengths: HashMap<K, Info>, data: IoBufMut) -> Self {
42        Self {
43            blob,
44            version,
45            lengths,
46            modified: BTreeSet::new(),
47            data,
48        }
49    }
50
51    /// Create a new empty [Wrapper].
52    fn empty(blob: B) -> Self {
53        Self {
54            blob,
55            version: 0,
56            lengths: HashMap::new(),
57            modified: BTreeSet::new(),
58            data: IoBufMut::default(),
59        }
60    }
61}
62
63/// State used during [Metadata::sync] operations.
64struct State<B: Blob, K: Span> {
65    cursor: usize,
66    next_version: u64,
67    key_order_changed: u64,
68    blobs: [Wrapper<B, K>; 2],
69    /// The completion of the last started sync, until observed.
70    ///
71    /// At most one sync is ever in flight: a new sync always targets the copy the pending sync
72    /// left as last-known-durable, so it must first prove the pending sync completed.
73    pending: Option<SyncCompletion>,
74}
75
76/// The store's state, boxed so the public [Metadata] handle stays pointer-sized.
77struct Inner<E: Context, K: Span, V: Codec> {
78    context: E,
79
80    map: BTreeMap<K, V>,
81    partition: String,
82    state: State<E::Blob, K>,
83
84    sync_overwrites: Counter,
85    sync_rewrites: Counter,
86    keys: Gauge,
87}
88
89/// One copy of the store, as loaded at startup.
90enum Loaded<B: Blob, K: Span, V> {
91    /// The copy decoded cleanly (an empty blob decodes to an empty map).
92    Valid(BTreeMap<K, V>, Wrapper<B, K>),
93    /// The copy holds bytes that fail validation.
94    Invalid(B),
95}
96
97impl<E: Context, K: Span, V: Codec> Inner<E, K, V> {
98    /// See [Metadata::init].
99    async fn init(context: E, cfg: Config<V::Cfg>) -> Result<Self, Error> {
100        // Open dedicated blobs
101        let (left_blob, left_len) = context.open(&cfg.partition, BLOB_NAMES[0]).await?;
102        let (right_blob, right_len) = context.open(&cfg.partition, BLOB_NAMES[1]).await?;
103
104        // Find latest blob (check which includes a hash of the other). Syncs alternate copies
105        // and drain the previous sync first, so at most one copy is ever mid-write: both copies
106        // failing validation is corruption, and adopting a fresh store would mask it.
107        let left = Self::load(&cfg.codec_config, 0, left_blob, left_len).await?;
108        let right = Self::load(&cfg.codec_config, 1, right_blob, right_len).await?;
109        if matches!((&left, &right), (Loaded::Invalid(_), Loaded::Invalid(_))) {
110            return Err(Error::Corruption(
111                "both metadata copies failed validation".into(),
112            ));
113        }
114        let (left_map, left_wrapper) = Self::normalize(left).await?;
115        let (right_map, right_wrapper) = Self::normalize(right).await?;
116
117        // Choose latest blob
118        let mut map = left_map;
119        let mut cursor = 0;
120        let mut version = left_wrapper.version;
121        if right_wrapper.version > left_wrapper.version {
122            cursor = 1;
123            map = right_map;
124            version = right_wrapper.version;
125        }
126        let next_version = version.checked_add(1).expect("version overflow");
127
128        // Create metrics
129        let sync_rewrites =
130            context.counter("sync_rewrites", "number of syncs that rewrote all data");
131        let sync_overwrites = context.counter(
132            "sync_overwrites",
133            "number of syncs that modified existing data",
134        );
135        let keys = context.gauge("keys", "number of tracked keys");
136
137        // Return metadata
138        let _ = keys.try_set(map.len());
139        Ok(Self {
140            context,
141
142            map,
143            partition: cfg.partition,
144            state: State {
145                cursor,
146                next_version,
147                key_order_changed: next_version, // rewrite on startup because we don't have a diff record
148                blobs: [left_wrapper, right_wrapper],
149                pending: None,
150            },
151
152            sync_rewrites,
153            sync_overwrites,
154            keys,
155        })
156    }
157
158    async fn load(
159        codec_config: &V::Cfg,
160        index: usize,
161        blob: E::Blob,
162        len: u64,
163    ) -> Result<Loaded<E::Blob, K, V>, Error> {
164        // Get blob length
165        if len == 0 {
166            // Empty blob
167            return Ok(Loaded::Valid(BTreeMap::new(), Wrapper::empty(blob)));
168        }
169
170        // The encoded buffer is retained after decoding, so pages read here need not remain
171        // in the OS page cache.
172        let len: usize = len.try_into().expect("blob too large for platform");
173        let buf = blob
174            .read_at(0, len, ReadOptions::DONT_CACHE)
175            .await?
176            .coalesce();
177
178        // Verify integrity.
179        //
180        // 8 bytes for version + 4 bytes for checksum.
181        if buf.len() < 8 + crc32::Digest::SIZE {
182            warn!(blob = index, len = buf.len(), "blob is too short");
183            return Ok(Loaded::Invalid(blob));
184        }
185
186        // Extract checksum
187        let bytes = buf.as_ref();
188        let checksum_index = buf.len() - crc32::Digest::SIZE;
189        let stored_checksum = u32::from_be_bytes(bytes[checksum_index..].try_into().unwrap());
190        let computed_checksum = Crc32::checksum(&bytes[..checksum_index]);
191        if stored_checksum != computed_checksum {
192            warn!(
193                blob = index,
194                stored = stored_checksum,
195                computed = computed_checksum,
196                "checksum mismatch"
197            );
198            return Ok(Loaded::Invalid(blob));
199        }
200
201        // Get parent
202        let version = u64::from_be_bytes(bytes[..8].try_into().unwrap());
203
204        // Extract data
205        //
206        // If the checksum is correct, we assume data is correctly packed and we don't perform
207        // length checks on the cursor.
208        let mut data = BTreeMap::new();
209        let mut lengths = HashMap::new();
210        let mut cursor = u64::SIZE;
211        while cursor < checksum_index {
212            // Read key
213            let key =
214                K::read(&mut Copying(&bytes[cursor..])).expect("unable to read key from blob");
215            cursor += key.encode_size();
216
217            // Read value
218            let value = V::read_cfg(&mut Copying(&bytes[cursor..]), codec_config)
219                .expect("unable to read value from blob");
220            lengths.insert(key.clone(), Info::new(cursor, value.encode_size()));
221            cursor += value.encode_size();
222            data.insert(key, value);
223        }
224
225        Ok(Loaded::Valid(
226            data,
227            Wrapper::new(blob, version, lengths, buf),
228        ))
229    }
230
231    /// Adopt a valid copy, or durably reset the one copy a crash left mid-write.
232    async fn normalize(
233        copy: Loaded<E::Blob, K, V>,
234    ) -> Result<(BTreeMap<K, V>, Wrapper<E::Blob, K>), Error> {
235        match copy {
236            Loaded::Valid(map, wrapper) => Ok((map, wrapper)),
237            Loaded::Invalid(blob) => {
238                blob.resize(0).await?;
239                blob.sync().await?;
240                Ok((BTreeMap::new(), Wrapper::empty(blob)))
241            }
242        }
243    }
244
245    /// See [Metadata::get].
246    fn get(&self, key: &K) -> Option<&V> {
247        self.map.get(key)
248    }
249
250    /// See [Metadata::get_mut].
251    fn get_mut(&mut self, key: &K) -> Option<&mut V> {
252        // Get value
253        let value = self.map.get_mut(key)?;
254
255        // Mark key as modified.
256        //
257        // We need to mark both blobs as modified because we may need to update both files.
258        let cursor = self.state.cursor;
259        self.state.blobs[cursor].modified.insert(key.clone());
260        self.state.blobs[1 - cursor].modified.insert(key.clone());
261
262        Some(value)
263    }
264
265    /// See [Metadata::clear].
266    fn clear(&mut self) {
267        // Clear map
268        self.map.clear();
269
270        // Mark key order as changed
271        self.state.key_order_changed = self.state.next_version;
272        self.keys.set(0);
273    }
274
275    /// See [Metadata::put].
276    fn put(&mut self, key: K, value: V) -> Option<V> {
277        // Insert value, getting previous value if it existed
278        let previous = self.map.insert(key.clone(), value);
279
280        // Mark key as modified.
281        //
282        // We need to mark both blobs as modified because we may need to update both files.
283        if previous.is_some() {
284            let cursor = self.state.cursor;
285            self.state.blobs[cursor].modified.insert(key.clone());
286            self.state.blobs[1 - cursor].modified.insert(key);
287        } else {
288            self.state.key_order_changed = self.state.next_version;
289        }
290        let _ = self.keys.try_set(self.map.len());
291        previous
292    }
293
294    /// See [Metadata::upsert].
295    fn upsert(&mut self, key: K, f: impl FnOnce(&mut V))
296    where
297        V: Default,
298    {
299        if let Some(value) = self.get_mut(&key) {
300            // Update existing value
301            f(value);
302        } else {
303            // Insert new value
304            let mut value = V::default();
305            f(&mut value);
306            self.put(key, value);
307        }
308    }
309
310    /// See [Metadata::remove].
311    fn remove(&mut self, key: &K) -> Option<V> {
312        // Get value
313        let past = self.map.remove(key);
314
315        // Mark key as modified.
316        if past.is_some() {
317            self.state.key_order_changed = self.state.next_version;
318        }
319        let _ = self.keys.try_set(self.map.len());
320
321        past
322    }
323
324    /// See [Metadata::keys].
325    fn keys(&self) -> impl Iterator<Item = &K> {
326        self.map.keys()
327    }
328
329    /// See [Metadata::retain].
330    fn retain(&mut self, mut f: impl FnMut(&K, &V) -> bool) {
331        // Retain only keys that satisfy the predicate
332        let old_len = self.map.len();
333        self.map.retain(|k, v| f(k, v));
334        let new_len = self.map.len();
335
336        // If the number of keys has changed, mark the key order as changed
337        if new_len != old_len {
338            self.state.key_order_changed = self.state.next_version;
339            let _ = self.keys.try_set(self.map.len());
340        }
341    }
342
343    /// Wait for an in-flight sync started by [Metadata::start_sync], surfacing its failure.
344    async fn wait_for_pending(&mut self) -> Result<(), RError> {
345        // A failure is surfaced without writing: the failed copy's on-disk state is unknown,
346        // and a write to the other (only durable) copy could destroy both. The consuming
347        // caller destroys the store on the error.
348        let Some(completion) = &self.state.pending else {
349            return Ok(());
350        };
351        completion.clone().await?;
352        self.state.pending = None;
353        Ok(())
354    }
355
356    /// See [Metadata::sync].
357    async fn sync(&mut self) -> Result<(), RError> {
358        self.wait_for_pending().await?;
359        self.write_next_version(false).await?;
360        Ok(())
361    }
362
363    /// See [Metadata::start_sync].
364    async fn start_sync(&mut self) -> Result<Handle<()>, RError> {
365        self.wait_for_pending().await?;
366        self.write_next_version(true).await
367    }
368
369    /// Write and persist the next version of the store to the target blob.
370    async fn write_next_version(&mut self, pipelined: bool) -> Result<Handle<()>, RError> {
371        // Extract values we need
372        let cursor = self.state.cursor;
373        let next_version = self.state.next_version;
374        let key_order_changed = self.state.key_order_changed;
375
376        // Compute next version.
377        //
378        // While it is possible that extremely high-frequency updates to metadata could cause an
379        // eventual overflow of version, syncing once per millisecond would overflow in 584,942,417
380        // years.
381        let past_version = self.state.blobs[cursor].version;
382        let next_next_version = next_version.checked_add(1).expect("version overflow");
383
384        // Get target blob (the one we will modify)
385        let target_cursor = 1 - cursor;
386
387        // When key order is stable, each blob's modified set tracks the value
388        // deltas it has not yet received. If the target has none, the current
389        // cursor already points at a durable copy of the latest state and
390        // writing another version would only rotate blobs.
391        if key_order_changed < past_version && self.state.blobs[target_cursor].modified.is_empty() {
392            return Ok(Handle::ready(Ok(())));
393        }
394
395        // Update the state.
396        self.state.cursor = target_cursor;
397        self.state.next_version = next_next_version;
398
399        // Get a mutable reference to the target blob.
400        let target = &mut self.state.blobs[target_cursor];
401
402        // Determine if we can overwrite existing data in place, updating the
403        // encoded buffer for equal-size values as we go. If any value changes
404        // encoded length, subsequent offsets shift and the blob must be rebuilt.
405        let mut overwrite = true;
406        if key_order_changed < past_version {
407            for key in target.modified.iter() {
408                let info = target.lengths.get(key).expect("key must exist");
409                let new_value = self.map.get(key).expect("key must exist");
410                if info.length == new_value.encode_size() {
411                    // Overwrite existing value
412                    let start = info.start;
413                    let end = start + info.length;
414                    let mut buf = &mut target.data.as_mut()[start..end];
415                    new_value.write(&mut buf);
416                } else {
417                    // Rewrite all
418                    overwrite = false;
419                    break;
420                }
421            }
422        } else {
423            // If the key order has changed, we need to rewrite all data
424            overwrite = false;
425        }
426
427        // Overwrite existing data
428        if overwrite {
429            // Update version
430            (&mut target.data.as_mut()[0..u64::SIZE]).put_u64(next_version);
431
432            // Update checksum
433            let checksum_index = target.data.len() - crc32::Digest::SIZE;
434            let checksum = Crc32::checksum(&target.data.as_ref()[..checksum_index]);
435            (&mut target.data.as_mut()[checksum_index..]).put_u32(checksum);
436
437            // Freeze the encoded buffer so async writes can share it without copying, then recover
438            // the mutable buffer after all writes complete. Since we retain the buffer in memory,
439            // every write requests cache bypass.
440            let data = std::mem::take(&mut target.data).freeze();
441
442            // A store no larger than one blob page is written whole: its changed values, version,
443            // and checksum share that page, so one write replaces several.
444            let full_write = data.len() <= Widen::widen(BLOB_PAGE_SIZE);
445            if full_write {
446                // This write includes every changed byte, so a non-pipelined sync can request
447                // durability with it.
448                let options = if pipelined {
449                    WriteOptions::DONT_CACHE
450                } else {
451                    WriteOptions::SYNC | WriteOptions::DONT_CACHE
452                };
453                target.blob.write_at(0, data.clone(), options).await?;
454            } else {
455                // Larger metadata stores write only modified values, the version, and the checksum.
456                let writes = target
457                    .modified
458                    .iter()
459                    .map(|key| {
460                        let info = target.lengths.get(key).expect("key must exist");
461                        let start = info.start;
462                        let end = start + info.length;
463                        target.blob.write_at(
464                            Widen::widen(start),
465                            data.slice(start..end),
466                            WriteOptions::DONT_CACHE,
467                        )
468                    })
469                    .chain([
470                        target
471                            .blob
472                            .write_at(0, data.slice(0..u64::SIZE), WriteOptions::DONT_CACHE),
473                        target.blob.write_at(
474                            Widen::widen(checksum_index),
475                            data.slice(checksum_index..checksum_index + crc32::Digest::SIZE),
476                            WriteOptions::DONT_CACHE,
477                        ),
478                    ]);
479                try_join_all(writes).await?;
480            }
481            let sync = if pipelined {
482                Some(target.blob.start_sync().await)
483            } else if full_write {
484                // The full write above already persisted the store.
485                None
486            } else {
487                target.blob.sync().await?;
488                None
489            };
490
491            // Clear modified keys to avoid writing the same data
492            target.modified.clear();
493
494            // Update state
495            target.version = next_version;
496            target.data = IoBufMut::from(data);
497            self.sync_overwrites.inc();
498            return Ok(self.record_pending(sync));
499        }
500
501        // Clear modified keys to avoid writing the same data
502        target.modified.clear();
503
504        // Since we can't overwrite in place, we rewrite the entire blob.
505        // `IoBufMut` does not grow, so compute the final encoded length before
506        // selecting a destination buffer.
507        let mut lengths = HashMap::with_capacity(self.map.len());
508        let mut next_data_len = u64::SIZE + crc32::Digest::SIZE;
509        for (key, value) in &self.map {
510            let value_len = value.encode_size();
511            lengths.insert(key.clone(), Info::new(0, value_len));
512            next_data_len += key.encode_size() + value_len;
513        }
514
515        // Capture the old length before reusing this buffer so shrinking
516        // rewrites still resize the persisted blob.
517        let target_data_len = target.data.len();
518
519        // Reuse the encoded buffer when its allocation is already large enough.
520        let mut next_data = if target.data.capacity() >= next_data_len {
521            let mut data = std::mem::take(&mut target.data);
522            data.clear();
523            data
524        } else {
525            IoBufMut::with_capacity(next_data_len)
526        };
527        next_data.put_u64(next_version);
528
529        // Build new data
530        for (key, value) in &self.map {
531            key.write(&mut next_data);
532            let info = lengths.get_mut(key).expect("key must exist");
533            info.start = next_data.len();
534            value.write(&mut next_data);
535        }
536        next_data.put_u32(Crc32::checksum(next_data.as_ref()));
537
538        // Shrinking rewrites must also persist the resize, so they need a full sync.
539        let next_data = next_data.freeze();
540        let shrinking = next_data.len() < target_data_len;
541
542        // We retain the encoded buffer in memory below, so every write requests cache bypass.
543        let sync = if pipelined {
544            target
545                .blob
546                .write_at(0, next_data.clone(), WriteOptions::DONT_CACHE)
547                .await?;
548            if shrinking {
549                target.blob.resize(next_data.len() as u64).await?;
550            }
551            Some(target.blob.start_sync().await)
552        } else if shrinking {
553            target
554                .blob
555                .write_at(0, next_data.clone(), WriteOptions::DONT_CACHE)
556                .await?;
557            target.blob.resize(next_data.len() as u64).await?;
558            target.blob.sync().await?;
559            None
560        } else {
561            // Non-shrinking rewrites are a single write and can use range-scoped
562            // durability.
563            target
564                .blob
565                .write_at(
566                    0,
567                    next_data.clone(),
568                    WriteOptions::SYNC | WriteOptions::DONT_CACHE,
569                )
570                .await?;
571            None
572        };
573
574        // Update blob state
575        target.version = next_version;
576        target.lengths = lengths;
577        target.data = IoBufMut::from(next_data);
578
579        self.sync_rewrites.inc();
580        Ok(self.record_pending(sync))
581    }
582
583    /// Record a started blob sync (if any) as the pending sync and return its observer handle.
584    fn record_pending(&mut self, sync: Option<Handle<()>>) -> Handle<()> {
585        let Some(sync) = sync else {
586            return Handle::ready(Ok(()));
587        };
588        let completion: SyncCompletion = sync.boxed().shared();
589        let handle = Handle::from_future(completion.clone());
590        self.state.pending = Some(completion);
591        handle
592    }
593
594    /// See [Metadata::destroy].
595    async fn destroy(mut self) -> Result<(), Error> {
596        // The removal order below relies on the cursor naming the newest durable copy, which a
597        // failed started sync breaks, so that failure is surfaced before anything is removed.
598        self.wait_for_pending().await?;
599
600        // Remove the older copy first. A reopen after an interrupted destroy then finds the
601        // newest generation or an empty store, never a superseded generation.
602        let State { cursor, blobs, .. } = self.state;
603        drop(blobs);
604        for i in [1 - cursor, cursor] {
605            self.context
606                .remove(&self.partition, Some(BLOB_NAMES[i]))
607                .await?;
608            debug!(blob = i, "destroyed blob");
609        }
610        match self.context.remove(&self.partition, None).await {
611            Ok(()) => {}
612            Err(RError::PartitionMissing(_)) => {
613                // Partition already removed or never existed.
614            }
615            Err(err) => return Err(Error::Runtime(err)),
616        }
617        Ok(())
618    }
619}
620
621/// Implementation of [Metadata] storage.
622///
623/// Storage-mutating functions consume the store and return it only on success: an error (or a
624/// dropped future) destroys the handle.
625pub struct Metadata<E: Context, K: Span, V: Codec>(Box<Inner<E, K, V>>);
626
627impl<E: Context, K: Span, V: Codec> std::fmt::Debug for Metadata<E, K, V> {
628    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
629        f.debug_struct("Metadata")
630            .field("keys", &self.0.map.len())
631            .finish_non_exhaustive()
632    }
633}
634
635impl<E: Context, K: Span, V: Codec> Metadata<E, K, V> {
636    /// Initialize a new [Metadata] instance.
637    pub async fn init(context: E, cfg: Config<V::Cfg>) -> Result<Self, Error> {
638        Ok(Self(Box::new(Inner::init(context, cfg).await?)))
639    }
640
641    /// Get a value from [Metadata] (if it exists).
642    pub fn get(&self, key: &K) -> Option<&V> {
643        self.0.get(key)
644    }
645
646    /// Get a mutable reference to a value from [Metadata] (if it exists).
647    pub fn get_mut(&mut self, key: &K) -> Option<&mut V> {
648        self.0.get_mut(key)
649    }
650
651    /// Clear all values from [Metadata]. The new state will not be persisted until [Self::sync] is
652    /// called.
653    pub fn clear(&mut self) {
654        self.0.clear();
655    }
656
657    /// Put a value into [Metadata].
658    ///
659    /// If the key already exists, the value will be overwritten and the previous
660    /// value is returned. The value stored will not be persisted until [Self::sync]
661    /// is called.
662    pub fn put(&mut self, key: K, value: V) -> Option<V> {
663        self.0.put(key, value)
664    }
665
666    /// Perform a [Self::put] and [Self::sync] in a single operation.
667    ///
668    /// Like calling [Self::sync] directly, this commits all pending metadata
669    /// changes, not just the provided key.
670    pub async fn put_sync(mut self, key: K, value: V) -> Result<Self, Error> {
671        self.0.put(key, value);
672        self.0.sync().await?;
673        Ok(self)
674    }
675
676    /// Update (or insert) a value in [Metadata] using a closure.
677    pub fn upsert(&mut self, key: K, f: impl FnOnce(&mut V))
678    where
679        V: Default,
680    {
681        self.0.upsert(key, f);
682    }
683
684    /// Update (or insert) a value in [Metadata] using a closure and sync immediately.
685    pub async fn upsert_sync(mut self, key: K, f: impl FnOnce(&mut V)) -> Result<Self, Error>
686    where
687        V: Default,
688    {
689        self.0.upsert(key, f);
690        self.0.sync().await?;
691        Ok(self)
692    }
693
694    /// Remove a value from [Metadata] (if it exists).
695    pub fn remove(&mut self, key: &K) -> Option<V> {
696        self.0.remove(key)
697    }
698
699    /// Iterate over all keys in metadata.
700    pub fn keys(&self) -> impl Iterator<Item = &K> {
701        self.0.keys()
702    }
703
704    /// Retain only the keys that satisfy the predicate.
705    pub fn retain(&mut self, f: impl FnMut(&K, &V) -> bool) {
706        self.0.retain(f);
707    }
708
709    /// Atomically commit the current state of [Metadata].
710    pub async fn sync(mut self) -> Result<Self, Error> {
711        self.0.sync().await?;
712        Ok(self)
713    }
714
715    /// Atomically begin committing the current state of [Metadata], returning a completion handle.
716    ///
717    /// Awaiting the returned [Handle] provides the same guarantee as [Self::sync]. A started
718    /// sync's failure surfaces on the handle and again on the next sync, which fails (destroying
719    /// the store) without writing. At most one sync is in flight: a new call writes nothing
720    /// until the prior sync completes. Dropping the handle neither cancels the sync nor loses a
721    /// failure.
722    pub async fn start_sync(mut self) -> Result<(Self, Handle<()>), Error> {
723        let handle = self.0.start_sync().await?;
724        Ok((self, handle))
725    }
726
727    /// Wait for a sync started by [Self::start_sync] to complete, surfacing its failure without
728    /// writing anything.
729    pub(crate) async fn wait_for_sync(&mut self) -> Result<(), Error> {
730        Ok(self.0.wait_for_pending().await?)
731    }
732
733    /// Remove the underlying blobs for this [Metadata].
734    ///
735    /// The older copy is removed first, so a reopen after an interrupted destroy finds either the
736    /// newest generation or an empty store, never a superseded generation. A started sync that
737    /// failed is reported before either copy is removed.
738    pub async fn destroy(self) -> Result<(), Error> {
739        self.0.destroy().await
740    }
741}