offline_protocol/protocol/data.rs
1//! The replicated-document store: persistence, caps, and compaction.
2//!
3//! The engine lives in `offline-protocol-data` and knows nothing about
4//! storage. This module is the other half: it decides when a document is
5//! read, when its changes reach disk, and when history is folded away. It
6//! owns no cryptography of its own — records go through the same sealing
7//! chokepoint every other protocol-state category uses, which is what makes
8//! a swapped-in storage backend unable to change the at-rest posture.
9//!
10//! # Record layout
11//!
12//! | Category | Key id | Contents |
13//! |---|---|---|
14//! | `data_docs` | `{space}/{doc}` | The compacted document |
15//! | `data_delta_log` | `{space}/{doc}/{seq:016x}` | One persisted commit |
16//! | `data_spaces` | `{space}` | The document index for the space |
17//! | `data_doc_meta` | `{space}/{doc}` | What a name says after a removal |
18//!
19//! # Why the listing is the truth and the index is a cache
20//!
21//! Everything needed to re-open a document is derived from what is actually
22//! on disk: `list_keys` names the delta records, and their zero-padded hex
23//! sequence sorts into apply order. The space record only remembers document
24//! names, so a document created and never written still lists. Any crash
25//! therefore leaves a readable store rather than a bookkeeping claim that
26//! has to be reconciled against reality.
27//!
28//! Removals are the one thing the listing cannot express, and the reason
29//! `data_doc_meta` exists. A removed document has no records, and neither
30//! does a document nobody ever created: reading the truth off the listing
31//! would make those two the same state, which is exactly what lets a peer's
32//! next offer refill something this device deleted on purpose. The removal
33//! record is therefore written *before* the records it removes, so a crash
34//! in between leaves a name already marked removed rather than a name whose
35//! surviving records vote it back into existence.
36
37use std::collections::{BTreeMap, BTreeSet};
38use std::sync::Arc;
39
40use offline_protocol_data::{
41 policy, CatchUp, DataDoc, DataError, DataValue, RemoteImport, VersionToken,
42};
43use serde::{Deserialize, Serialize};
44use tracing::{debug, warn};
45
46use crate::error::{Error, Result};
47use crate::events::{DocRemovedBy, Event};
48use crate::protocol::data_sync::MAX_REMOVAL_FLOOR_BYTES;
49use crate::protocol::storage::StateRecord;
50use crate::protocol::types::{storage_keys, MAX_PROTOCOL_STATE_RECORD_BYTES};
51use crate::protocol::OfflineProtocol;
52use crate::protocol_state_storage::ProtocolStateStorage;
53
54/// How many interest patterns one space accepts.
55///
56/// A bound rather than a policy: the list is walked once per document per
57/// offer, so an unbounded one would let an application make its own peers
58/// expensive to answer.
59pub(crate) const MAX_INTEREST_PATTERNS: usize = 32;
60
61/// Whether one interest pattern matches a document name.
62///
63/// `*` alone is everything; a trailing `*` is a prefix; anything else is an
64/// exact name. The document charset (`A-Z a-z 0-9 . _ -`) has no `*` in it,
65/// so a wildcard is never ambiguous with a name that contains one.
66pub(crate) fn interest_matches(pattern: &str, doc: &str) -> bool {
67 match pattern.strip_suffix('*') {
68 Some(prefix) => doc.starts_with(prefix),
69 None => pattern == doc,
70 }
71}
72
73/// Reject a pattern that could never match, or that names something the
74/// document charset cannot express.
75///
76/// Checked where it is written rather than where it is used, so an
77/// application hears about a typo at the call that made it instead of
78/// silently receiving nothing.
79fn validate_interest_pattern(pattern: &str) -> Result<()> {
80 // `*` alone is the default expressed explicitly, and it is the one
81 // pattern whose body is empty.
82 if pattern == "*" {
83 return Ok(());
84 }
85 // Everything else is a document name with an optional trailing `*`, so
86 // the document validator judges it rather than a second copy of the
87 // charset. A copy would drift the day `validate_name` widens, and the
88 // symptom would be a pattern that cannot match a name the store accepts.
89 let body = pattern.strip_suffix('*').unwrap_or(pattern);
90 offline_protocol_data::validate_name(body).map_err(|err| {
91 Error::InvalidArgument(format!(
92 "an interest pattern is a document name with an optional \
93 trailing `*`: {pattern:?} {err}"
94 ))
95 })
96}
97
98/// The document index for one space.
99///
100/// Deliberately thin. Sizes and sequence numbers are derived from the
101/// records themselves at open, so this record can never disagree with the
102/// store about what exists; it only remembers names, which nothing else
103/// records for a document that has never been written.
104#[derive(Debug, Clone, Default, Serialize, Deserialize)]
105struct SpaceRecord {
106 #[serde(default)]
107 docs: BTreeSet<String>,
108}
109
110/// What a document's name says after a removal.
111///
112/// The `gone` field is the removal *floor*: the version the document stood
113/// at when it was removed, which is the whole of what a removal decided
114/// about. Content at or below it was removed; content beyond it is an edit
115/// made concurrently with the removal or after it, which the removal never
116/// saw. That is what makes an edit win over a removal without either side
117/// needing to know which happened first.
118///
119/// `alive` is separate from the floor on purpose, and is not derivable from
120/// whether records exist. A crash between the removal record and the
121/// deletes leaves records behind, and reading those as "alive" is precisely
122/// the resurrection this record prevents. The floor survives a document
123/// coming back, so a stale replica's copy of the removed content is still
124/// refused afterwards.
125#[derive(Debug, Clone, Serialize, Deserialize)]
126struct DocMeta {
127 /// Base64 of the version the document was removed at.
128 gone: String,
129 /// Whether the name holds a document again.
130 ///
131 /// Defaults false so a record that fails to carry the field reads as
132 /// removed, which is the answer that loses no data: a document read as
133 /// removed comes back from any replica that still holds it, while one
134 /// wrongly read as live is a resurrection nothing reports.
135 #[serde(default)]
136 alive: bool,
137}
138
139/// What applying a peer's removal did to this device's copy.
140#[derive(Debug, Clone, Copy, PartialEq, Eq)]
141pub(crate) enum TombstoneOutcome {
142 /// The copy held here was content the floor covered, and is gone.
143 Deleted,
144 /// The copy held here carries an edit the removal never saw, so it
145 /// stays and goes back to the peer through the ordinary exchange.
146 Kept,
147 /// Nothing was held under that name; the floor is recorded so a replica
148 /// that still holds the removed content cannot seed it back.
149 Recorded,
150 /// A floor already held here covers this one, so it carried nothing
151 /// new and nothing was written or opened. The common case: floors
152 /// travel on every offer for the life of the space.
153 Unchanged,
154}
155
156/// A document held open in memory.
157struct LoadedDoc {
158 doc: DataDoc,
159 /// Sequence number the next persisted commit will use.
160 next_seq: u64,
161 /// Total size of the delta records currently on disk for this document.
162 log_bytes: usize,
163 /// Size of the last compacted record written for this document.
164 compacted_bytes: usize,
165 /// Set when the document's compacted encoding passed the cap.
166 ///
167 /// Growth is refused while it is set; deletions are not, so an
168 /// application that hit the cap can shrink its way back out. Cleared by
169 /// the next flush that measures under the cap.
170 over_cap: bool,
171 /// Whether every delta record on disk actually applied at open.
172 ///
173 /// False when a delta was unreadable, or when one imported but parked
174 /// behind a predecessor that never arrived. Both mean the document in
175 /// memory is missing changes the records still describe, and both make
176 /// compaction destructive: folding the log would write a snapshot without
177 /// those changes and then delete the records holding them. A growing log
178 /// is the cheaper failure, so compaction is simply switched off until an
179 /// open sees a complete log again.
180 history_complete: bool,
181}
182
183/// In-memory state for the data layer.
184#[derive(Default)]
185pub(crate) struct DataLayer {
186 docs: BTreeMap<(String, String), LoadedDoc>,
187 spaces: BTreeMap<String, SpaceRecord>,
188 /// Removal state per document, loaded with the space that holds it.
189 meta: BTreeMap<(String, String), DocMeta>,
190 /// What this device wants from its peers, per space.
191 ///
192 /// Absent means everything, which is what a space nobody has narrowed
193 /// says and what every release before interest existed said. Deliberately
194 /// not persisted: it is application policy about this launch, not a fact
195 /// about the store, and a durable copy would be a second thing to
196 /// reconcile against an application that has changed its mind.
197 interest: BTreeMap<String, Vec<String>>,
198}
199
200impl std::fmt::Debug for DataLayer {
201 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202 f.debug_struct("DataLayer")
203 .field("open_docs", &self.docs.len())
204 .field("loaded_spaces", &self.spaces.len())
205 .field("removed_docs", &self.meta.len())
206 .field("narrowed_spaces", &self.interest.len())
207 .finish()
208 }
209}
210
211fn map_data_error(err: DataError) -> Error {
212 match err {
213 DataError::DocTooLarge { actual, limit } => Error::DocTooLarge { actual, limit },
214 DataError::Corrupt(detail) | DataError::Engine(detail) => Error::DataCorrupted(detail),
215 DataError::Poisoned => {
216 Error::DataCorrupted("document handle poisoned by an earlier decode failure".into())
217 }
218 DataError::InvalidName { name, reason } => {
219 Error::InvalidArgument(format!("invalid name {name:?}: {reason}"))
220 }
221 DataError::ValueTooLarge { actual, limit } => Error::InvalidArgument(format!(
222 "value is {actual} bytes, over the {limit} byte limit"
223 )),
224 DataError::InvalidAttachment { reason } => {
225 Error::InvalidArgument(format!("invalid attachment: {reason}"))
226 }
227 DataError::OutOfRange { position, length } => Error::InvalidArgument(format!(
228 "position {position} is out of range for length {length}"
229 )),
230 // Forced by `#[non_exhaustive]` on `DataError`, which the data crate
231 // carries so a new variant is not a semver break for published
232 // consumers. That trade costs the compile error that would otherwise
233 // catch an unmapped variant, so it is replaced with a `warn!`: an
234 // unmapped variant shows up in logs instead of vanishing. When you add
235 // a variant, add its arm here — this is a tripwire, not a destination.
236 ref other => {
237 warn!(
238 error = %other,
239 "unmapped DataError variant reached the protocol boundary; \
240 add an explicit arm in protocol/data.rs"
241 );
242 Error::Other(other.to_string())
243 }
244 }
245}
246
247/// Compose the record key for a document's compacted snapshot.
248fn doc_key(space: &str, doc: &str) -> String {
249 format!("{space}/{doc}")
250}
251
252/// Compose the record key for one persisted commit.
253///
254/// The sequence is zero-padded hex so that lexicographic order — which is
255/// the only order `list_keys` promises anything about — is apply order.
256fn delta_key(space: &str, doc: &str, seq: u64) -> String {
257 format!("{space}/{doc}/{seq:016x}")
258}
259
260/// Parse the sequence out of a delta record key belonging to this document.
261fn delta_seq(key: &str, prefix: &str) -> Option<u64> {
262 let suffix = key.strip_prefix(prefix)?;
263 u64::from_str_radix(suffix, 16).ok()
264}
265
266impl OfflineProtocol {
267 /// The backend documents are stored in.
268 ///
269 /// The application's override when it set one, and otherwise the store
270 /// protocol state already uses — which is what makes the layer work with
271 /// no storage configuration at all.
272 fn data_storage(&self) -> Option<Arc<dyn ProtocolStateStorage>> {
273 self.config
274 .data
275 .storage
276 .clone()
277 .or_else(|| self.protocol_state_storage.clone())
278 }
279
280 /// The backend, or the typed error explaining which precondition failed.
281 fn require_data_storage(&self) -> Result<Arc<dyn ProtocolStateStorage>> {
282 if !self.config.data.enabled {
283 return Err(Error::DataDisabled);
284 }
285 self.data_storage().ok_or(Error::DataStorageUnavailable)
286 }
287
288 fn validate_ids(space: &str, doc: &str) -> Result<()> {
289 offline_protocol_data::validate_space_name(space).map_err(map_data_error)?;
290 offline_protocol_data::validate_name(doc).map_err(map_data_error)?;
291 Ok(())
292 }
293
294 /// Read a space's document index, loading it on first touch.
295 fn load_space(&mut self, storage: &dyn ProtocolStateStorage, space: &str) -> Result<()> {
296 if self.data.spaces.contains_key(space) {
297 return Ok(());
298 }
299 let mut record = SpaceRecord::default();
300 match self.read_state_record(storage, storage_keys::DATA_SPACES, space) {
301 Ok(Some(bytes)) => match serde_json::from_slice::<SpaceRecord>(&bytes) {
302 Ok(parsed) => record = parsed,
303 Err(err) => {
304 // The index is a cache: a damaged one costs the names of
305 // documents that have no records yet, never a document.
306 warn!(space, error = %err, "Unreadable data space index; rebuilding from records");
307 }
308 },
309 Ok(None) => {}
310 Err(err) => {
311 warn!(space, error = %err, "Failed to read data space index");
312 }
313 }
314
315 // Reconcile against what is actually stored, so the index can never
316 // hide a document that exists.
317 for key_type in [storage_keys::DATA_DOCS, storage_keys::DATA_DELTA_LOG] {
318 if let Ok(keys) = storage.list_keys(key_type) {
319 for key in keys {
320 if let Some(rest) = key.strip_prefix(&format!("{space}/")) {
321 let name = rest.split('/').next().unwrap_or(rest);
322 if !name.is_empty() {
323 record.docs.insert(name.to_string());
324 }
325 }
326 }
327 }
328 }
329
330 // Removals last, because they overrule the listing. A name the
331 // reconciliation above just voted back into existence may be one a
332 // previous run removed and did not finish deleting, and the records
333 // it left are what that vote was counted from.
334 let mut half_removed = Vec::new();
335 if let Ok(keys) = storage.list_keys(storage_keys::DATA_DOC_META) {
336 let prefix = format!("{space}/");
337 for key in keys {
338 let Some(name) = key.strip_prefix(&prefix) else {
339 continue;
340 };
341 if name.is_empty() || name.contains('/') {
342 continue;
343 }
344 let meta = match self.read_state_record(storage, storage_keys::DATA_DOC_META, &key)
345 {
346 Ok(Some(bytes)) => match serde_json::from_slice::<DocMeta>(&bytes) {
347 Ok(meta) => meta,
348 Err(err) => {
349 // Unreadable here is not the index's "costs a
350 // name" case: this record is the only thing that
351 // says a document was removed, so a damaged one
352 // is left in place and the name is left alone
353 // rather than being silently revived.
354 warn!(space, doc = name, error = %err, "Unreadable document removal record");
355 continue;
356 }
357 },
358 Ok(None) => continue,
359 Err(err) => {
360 warn!(space, doc = name, error = %err, "Failed to read a document removal record");
361 continue;
362 }
363 };
364 if !meta.alive && record.docs.remove(name) {
365 half_removed.push(name.to_string());
366 }
367 self.data
368 .meta
369 .insert((space.to_string(), name.to_string()), meta);
370 }
371 }
372
373 self.data.spaces.insert(space.to_string(), record);
374
375 // Finish what a crash interrupted. Best effort, and repeated on
376 // every open until it succeeds: the name is already removed as far
377 // as every reader is concerned, so what is left is storage to
378 // reclaim rather than a decision to make.
379 for doc in half_removed {
380 debug!(
381 space,
382 doc, "Finishing a removal a previous run did not complete"
383 );
384 self.purge_doc_records(storage, space, &doc, None);
385 }
386 Ok(())
387 }
388
389 /// Delete every record belonging to one document, reporting the first
390 /// failure and attempting the rest regardless.
391 ///
392 /// A record that survives is replayed into the *next* document of the
393 /// same name, which resurrects the removed contents, so a partial
394 /// failure is worth hearing about even though every caller here can only
395 /// log it.
396 ///
397 /// `delta_keys` is the delta log's listing when the caller already holds
398 /// one, so a caller purging many documents lists once rather than once
399 /// per document. It has to postdate the document's last flush, or the
400 /// delta that flush wrote is exactly the record that survives.
401 fn purge_doc_records(
402 &mut self,
403 storage: &dyn ProtocolStateStorage,
404 space: &str,
405 doc: &str,
406 delta_keys: Option<&[String]>,
407 ) -> Option<String> {
408 fn note(
409 first_error: &mut Option<String>,
410 err: crate::protocol_state_storage::ProtocolStateError,
411 ) {
412 if first_error.is_none() {
413 *first_error = Some(err.to_string());
414 }
415 }
416 let mut first_error: Option<String> = None;
417
418 if let Err(err) = storage.delete(storage_keys::DATA_DOCS, &doc_key(space, doc)) {
419 note(&mut first_error, err);
420 }
421 let prefix = format!("{space}/{doc}/");
422 let listed;
423 let keys: &[String] = match delta_keys {
424 Some(keys) => keys,
425 None => match storage.list_keys(storage_keys::DATA_DELTA_LOG) {
426 Ok(keys) => {
427 listed = keys;
428 &listed
429 }
430 Err(err) => {
431 note(&mut first_error, err);
432 &[]
433 }
434 },
435 };
436 for key in keys.iter().filter(|key| key.starts_with(&prefix)) {
437 if let Err(err) = storage.delete(storage_keys::DATA_DELTA_LOG, key) {
438 note(&mut first_error, err);
439 }
440 }
441 first_error
442 }
443
444 /// Write one document's removal record.
445 fn persist_doc_meta(
446 &mut self,
447 storage: &dyn ProtocolStateStorage,
448 space: &str,
449 doc: &str,
450 meta: &DocMeta,
451 ) -> Result<()> {
452 let bytes =
453 serde_json::to_vec(meta).map_err(|err| Error::Serialization(err.to_string()))?;
454 self.write_state_record(
455 storage,
456 storage_keys::DATA_DOC_META,
457 &doc_key(space, doc),
458 &bytes,
459 )
460 .map_err(|err| {
461 Error::Other(format!(
462 "failed to record the removal of {space}/{doc}: {err}"
463 ))
464 })
465 }
466
467 /// Write one document's removal record and then remember it, in that
468 /// order: what memory says must never be ahead of what a relaunch will
469 /// read.
470 fn set_doc_meta(
471 &mut self,
472 storage: &dyn ProtocolStateStorage,
473 space: &str,
474 doc: &str,
475 meta: DocMeta,
476 ) -> Result<()> {
477 self.persist_doc_meta(storage, space, doc, &meta)?;
478 self.data
479 .meta
480 .insert((space.to_string(), doc.to_string()), meta);
481 Ok(())
482 }
483
484 /// Whether this name currently stands removed.
485 ///
486 /// The space must already be loaded.
487 fn doc_is_removed(&self, space: &str, doc: &str) -> bool {
488 self.data
489 .meta
490 .get(&(space.to_string(), doc.to_string()))
491 .is_some_and(|meta| !meta.alive)
492 }
493
494 /// Let a removed name hold a document again, keeping its floor.
495 ///
496 /// The floor outlives the removal it recorded, because a replica that
497 /// never heard about the removal still holds the old contents and will
498 /// offer them back. Dropping the floor here would let exactly that
499 /// offer refill the new document with the old one.
500 fn revive_doc(
501 &mut self,
502 storage: &dyn ProtocolStateStorage,
503 space: &str,
504 doc: &str,
505 ) -> Result<()> {
506 let key = (space.to_string(), doc.to_string());
507 let Some(meta) = self.data.meta.get(&key) else {
508 return Ok(());
509 };
510 if meta.alive {
511 return Ok(());
512 }
513 let revived = DocMeta {
514 gone: meta.gone.clone(),
515 alive: true,
516 };
517 self.set_doc_meta(storage, space, doc, revived)
518 }
519
520 /// The removal floor recorded for one document, if it has one.
521 ///
522 /// The space must already be loaded; every caller reaches this through a
523 /// path that loads it.
524 fn doc_floor(&self, space: &str, doc: &str) -> Option<VersionToken> {
525 use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
526
527 let meta = self.data.meta.get(&(space.to_string(), doc.to_string()))?;
528 match BASE64.decode(&meta.gone) {
529 Ok(bytes) => Some(VersionToken::from_bytes(bytes)),
530 Err(err) => {
531 warn!(space, doc, error = %err, "Undecodable removal floor; treating the document as never removed");
532 None
533 }
534 }
535 }
536
537 fn persist_space(&mut self, storage: &dyn ProtocolStateStorage, space: &str) -> Result<()> {
538 let Some(record) = self.data.spaces.get(space) else {
539 return Ok(());
540 };
541 let bytes =
542 serde_json::to_vec(record).map_err(|err| Error::Serialization(err.to_string()))?;
543 self.write_state_record(storage, storage_keys::DATA_SPACES, space, &bytes)
544 .map_err(|err| Error::Other(format!("failed to persist data space index: {err}")))
545 }
546
547 /// Open a document, loading its compacted record and delta log.
548 ///
549 /// Applying the compacted record and then every delta is safe in any
550 /// order and safe to repeat: duplicates are absorbed by the merge, which
551 /// is exactly why a crash at any point during compaction leaves a
552 /// recoverable store.
553 ///
554 /// A removed name is refused rather than opened empty. Opening it would
555 /// create a live document out of a removal, and every write to it would
556 /// then be a change to something no replica holds. Bringing the name
557 /// back is a decision with a caller: `data_create_doc` locally, or a
558 /// peer's change from beyond the removal floor.
559 fn open_doc(
560 &mut self,
561 storage: &dyn ProtocolStateStorage,
562 space: &str,
563 doc: &str,
564 ) -> Result<()> {
565 if self
566 .data
567 .docs
568 .contains_key(&(space.to_string(), doc.to_string()))
569 {
570 return Ok(());
571 }
572 self.load_space(storage, space)?;
573 if self.doc_is_removed(space, doc) {
574 return Err(Error::InvalidState(format!(
575 "document {space}/{doc} was removed; create it again to reuse the name"
576 )));
577 }
578
579 let mut document = DataDoc::new();
580 let mut compacted_bytes = 0usize;
581 let mut history_complete = true;
582
583 match self.read_state_record_detailed(
584 storage,
585 storage_keys::DATA_DOCS,
586 &doc_key(space, doc),
587 ) {
588 Ok(StateRecord::Present(bytes)) => {
589 compacted_bytes = bytes.len();
590 // A snapshot is self-contained, so parking here should not be
591 // reachable. Recorded rather than assumed away: if it ever is,
592 // the document is missing its own base and compaction would
593 // write that gap over the delta log.
594 if document.import(&bytes).map_err(map_data_error)?.is_parked() {
595 history_complete = false;
596 warn!(space, doc, "Document snapshot did not fully apply");
597 }
598 }
599 // `Unavailable` means it is still on disk and may be fine, so
600 // refusing here keeps a transient failure from looking like data
601 // loss to the application.
602 Ok(StateRecord::Unavailable) => {
603 return Err(Error::Other(format!(
604 "document {space}/{doc} could not be read this session"
605 )));
606 }
607 // `Unreadable` means the record was examined and is permanently
608 // gone; the delta log may still carry the document forward. The
609 // document has lost its base either way, so compaction stays off:
610 // folding now would write whatever partial state survived over the
611 // records that still hold the rest.
612 Ok(StateRecord::Unreadable) => {
613 history_complete = false;
614 warn!(space, doc, "Document snapshot is permanently unreadable");
615 }
616 Ok(StateRecord::Missing) => {}
617 Err(err) => {
618 return Err(Error::Other(format!("failed to read document: {err}")));
619 }
620 }
621
622 let prefix = format!("{space}/{doc}/");
623 let mut sequences: Vec<u64> = storage
624 .list_keys(storage_keys::DATA_DELTA_LOG)
625 .unwrap_or_default()
626 .iter()
627 .filter_map(|key| delta_seq(key, &prefix))
628 .collect();
629 sequences.sort_unstable();
630
631 let mut log_bytes = 0usize;
632 let mut next_seq = 0u64;
633 let mut replayed = 0u32;
634 for seq in sequences {
635 next_seq = next_seq.max(seq.saturating_add(1));
636 let key = delta_key(space, doc, seq);
637 match self.read_state_record_detailed(storage, storage_keys::DATA_DELTA_LOG, &key) {
638 Ok(StateRecord::Present(bytes)) => {
639 log_bytes = log_bytes.saturating_add(bytes.len());
640 replayed = replayed.saturating_add(1);
641 match document.import(&bytes) {
642 // The engine accepted the delta but cannot apply it:
643 // its predecessor is missing, so it and everything
644 // after it are invisible. Recording that is what stops
645 // compaction from writing a snapshot without these
646 // changes and then deleting the records holding them.
647 Ok(outcome) if outcome.is_parked() => {
648 history_complete = false;
649 warn!(
650 space,
651 doc,
652 seq,
653 "Document delta is waiting on a predecessor that never \
654 arrived; compaction is disabled for this document"
655 );
656 }
657 Ok(_) => {}
658 Err(err) => {
659 // A permanently undecodable delta is a hole. The
660 // document still opens with everything that did
661 // apply, but the log must not be folded away: F3
662 // resend is what closes a hole, and compaction
663 // would destroy what is left to resend against.
664 history_complete = false;
665 warn!(space, doc, seq, error = %err, "Skipping undecodable document delta");
666 }
667 }
668 }
669 // Still on disk and possibly perfectly good next launch.
670 // Refusing the open keeps a transient read failure from
671 // looking like data loss, exactly as the snapshot path above
672 // does, and keeps compaction away from records that
673 // everything after this delta depends on.
674 Ok(StateRecord::Unavailable) => {
675 return Err(Error::Other(format!(
676 "delta {seq} of document {space}/{doc} could not be read this session"
677 )));
678 }
679 // Listed but gone or permanently unreadable: a hole, not a
680 // reason to refuse the document.
681 Ok(_) => {
682 history_complete = false;
683 warn!(space, doc, seq, "Document delta is missing or unreadable");
684 }
685 Err(err) => {
686 return Err(Error::Other(format!(
687 "failed to read delta {seq} of document {space}/{doc}: {err}"
688 )));
689 }
690 }
691 }
692
693 document.mark_persisted();
694 if compacted_bytes == 0 {
695 compacted_bytes = document.measure().map_err(map_data_error)?;
696 }
697 // Hand the document what its own bookkeeping cannot know after a
698 // fresh open: how big the record it came from was, how much delta
699 // log sits on top, and how many commits that log represents. Skip
700 // this and a reopened document looks empty to the size check and to
701 // the compaction trigger, both of which then under-fire.
702 document.restore_bookkeeping(compacted_bytes, log_bytes, replayed);
703
704 if let Some(record) = self.data.spaces.get_mut(space) {
705 record.docs.insert(doc.to_string());
706 }
707 self.data.docs.insert(
708 (space.to_string(), doc.to_string()),
709 LoadedDoc {
710 doc: document,
711 next_seq,
712 log_bytes,
713 compacted_bytes,
714 over_cap: false,
715 history_complete,
716 },
717 );
718 Ok(())
719 }
720
721 /// Run an edit against an open document.
722 ///
723 /// `growth` marks operations that can make the document bigger. Those are
724 /// refused once the cap has been passed; deletions are not, so an
725 /// application that filled a document can still empty it.
726 fn with_doc<F>(&mut self, space: &str, doc: &str, growth: bool, edit: F) -> Result<()>
727 where
728 F: FnOnce(&mut DataDoc) -> std::result::Result<(), DataError>,
729 {
730 Self::validate_ids(space, doc)?;
731 let storage = self.require_data_storage()?;
732 self.open_doc(storage.as_ref(), space, doc)?;
733
734 let entry = self
735 .data
736 .docs
737 .get_mut(&(space.to_string(), doc.to_string()))
738 .ok_or_else(|| Error::Other("document vanished after open".to_string()))?;
739
740 if growth && entry.over_cap {
741 return Err(Error::DocTooLarge {
742 actual: entry.compacted_bytes,
743 limit: policy::MAX_DOC_BYTES,
744 });
745 }
746 edit(&mut entry.doc).map_err(map_data_error)
747 }
748
749 /// Read from an open document.
750 fn read_doc<F, T>(&mut self, space: &str, doc: &str, read: F) -> Result<T>
751 where
752 F: FnOnce(&DataDoc) -> std::result::Result<T, DataError>,
753 {
754 Self::validate_ids(space, doc)?;
755 let storage = self.require_data_storage()?;
756 self.open_doc(storage.as_ref(), space, doc)?;
757 let entry = self
758 .data
759 .docs
760 .get(&(space.to_string(), doc.to_string()))
761 .ok_or_else(|| Error::Other("document vanished after open".to_string()))?;
762 read(&entry.doc).map_err(map_data_error)
763 }
764
765 // ---- public surface ---------------------------------------------
766
767 /// Point documents at a backend the application supplies.
768 ///
769 /// Documents move; protocol secrets do not. Applied immediately, so a
770 /// store opened afterwards reads and writes through the new backend while
771 /// everything else stays where it is.
772 ///
773 /// Any document already open is written into the new backend as a
774 /// self-contained snapshot before this returns. That is not a
775 /// convenience, it is the whole correctness of a mid-session swap: a
776 /// delta record only describes the change *since* the previous one, so a
777 /// document that merely kept writing deltas into the new backend would
778 /// leave every earlier delta behind in the old one. The engine accepts
779 /// such an orphan delta and parks it, which means the document reads
780 /// **empty** from the new backend and the next compaction deletes the
781 /// orphans for good.
782 ///
783 /// If the migration cannot be written the swap does not happen at all and
784 /// the error is returned, because a half-swapped layer is precisely the
785 /// stranding this method exists to prevent.
786 ///
787 /// Documents that are not open are not migrated: they live wholly in the
788 /// old backend, which is what switching backends means.
789 pub fn set_data_storage(&mut self, storage: Arc<dyn ProtocolStateStorage>) -> Result<()> {
790 let previous = self.config.data.storage.take();
791 self.config.data.storage = Some(storage);
792
793 match self.migrate_open_docs_to_data_storage() {
794 Ok(()) => Ok(()),
795 Err(err) => {
796 self.config.data.storage = previous;
797 Err(err)
798 }
799 }
800 }
801
802 /// Write every open document into the current backend as a self-contained
803 /// snapshot, so nothing depends on history that lives somewhere else.
804 ///
805 /// Two phases, and the split is the correctness. Every write happens
806 /// first; only once all of them are durable does any document's
807 /// bookkeeping move. Interleaving the two would leave a document that
808 /// migrated before the failure claiming a fresh empty log, while the
809 /// caller rolls the swap back to the old backend: its next flush would
810 /// then write sequence zero over the delta already there and park
811 /// everything that followed it, which is permanent loss of exactly the
812 /// kind this migration exists to prevent.
813 fn migrate_open_docs_to_data_storage(&mut self) -> Result<()> {
814 let Some(storage) = self.data_storage() else {
815 return Ok(());
816 };
817
818 let keys: Vec<(String, String)> = self.data.docs.keys().cloned().collect();
819 let mut committed: Vec<(String, String)> = Vec::new();
820 let mut migrated: Vec<((String, String), usize)> = Vec::new();
821
822 if let Err(err) =
823 self.write_open_docs_into(storage.as_ref(), &keys, &mut committed, &mut migrated)
824 {
825 // Nothing durable is being claimed, so every export marker this
826 // advanced goes back. Leaving one advanced would drop the edits
827 // it covers from every future delta, which is the same silent
828 // loss a failed delta write already rewinds for.
829 for map_key in committed {
830 if let Some(entry) = self.data.docs.get_mut(&map_key) {
831 entry.doc.rewind_last_commit();
832 }
833 }
834 return Err(err);
835 }
836
837 // Phase two. Every record above is durable, so the swap is going to
838 // stand and the bookkeeping can follow it.
839 for (map_key, compacted_bytes) in migrated {
840 if let Some(entry) = self.data.docs.get_mut(&map_key) {
841 // The new backend holds one snapshot and no delta log.
842 entry.next_seq = 0;
843 entry.log_bytes = 0;
844 entry.compacted_bytes = compacted_bytes;
845 entry.doc.mark_compacted(compacted_bytes);
846 }
847 }
848 Ok(())
849 }
850
851 /// Phase one of a backend swap: every open document and space index
852 /// written into the new backend, with nothing in memory moved yet.
853 ///
854 /// `committed` collects the documents whose export marker this advanced,
855 /// so a failure can put every one of them back. `migrated` collects the
856 /// bookkeeping to apply once every write has succeeded.
857 fn write_open_docs_into(
858 &mut self,
859 storage: &dyn ProtocolStateStorage,
860 keys: &[(String, String)],
861 committed: &mut Vec<(String, String)>,
862 migrated: &mut Vec<((String, String), usize)>,
863 ) -> Result<()> {
864 for (space, doc) in keys {
865 let map_key = (space.clone(), doc.clone());
866
867 // Fold pending edits in first: the snapshot below has to be the
868 // whole document, including what has not been flushed yet.
869 {
870 let Some(entry) = self.data.docs.get_mut(&map_key) else {
871 continue;
872 };
873 if entry.doc.commit().map_err(map_data_error)?.is_some() {
874 committed.push(map_key.clone());
875 }
876 }
877 let compacted = {
878 let Some(entry) = self.data.docs.get(&map_key) else {
879 continue;
880 };
881 entry.doc.export_compacted().map_err(map_data_error)?
882 };
883
884 self.write_state_record(
885 storage,
886 storage_keys::DATA_DOCS,
887 &doc_key(space, doc),
888 &compacted,
889 )
890 .map_err(|err| {
891 Error::Other(format!(
892 "failed to move document {space}/{doc} into the new backend: {err}"
893 ))
894 })?;
895
896 // Snapshot first, then drop the log, in that order and for the
897 // same reason as compaction. A delta record already sitting under
898 // this name in the new backend belongs to some earlier document
899 // that held it: the snapshot just written does not descend from
900 // it, so at the next open it parks and switches compaction off
901 // for good. Best-effort, again as compaction: the snapshot is
902 // already durable, and a leftover costs a growing log rather than
903 // a wrong document.
904 let prefix = format!("{space}/{doc}/");
905 if let Ok(stale) = storage.list_keys(storage_keys::DATA_DELTA_LOG) {
906 for key in stale.iter().filter(|key| key.starts_with(&prefix)) {
907 let _ = storage.delete(storage_keys::DATA_DELTA_LOG, key);
908 }
909 }
910
911 migrated.push((map_key, compacted.len()));
912 }
913
914 // The space indexes carry document names that have no records of
915 // their own yet; without them the new backend forgets those names.
916 let spaces: Vec<String> = self.data.spaces.keys().cloned().collect();
917 for space in spaces {
918 self.persist_space(storage, &space)?;
919 }
920
921 // Removal records have no document to carry them, so a backend swap
922 // that left them behind would move a space into a store where every
923 // removed name reads as one nobody ever created, and the next offer
924 // from any peer would refill all of them.
925 let removals: Vec<((String, String), DocMeta)> = self
926 .data
927 .meta
928 .iter()
929 .map(|(key, meta)| (key.clone(), meta.clone()))
930 .collect();
931 for ((space, doc), meta) in removals {
932 self.persist_doc_meta(storage, &space, &doc, &meta)?;
933 }
934 Ok(())
935 }
936
937 /// Create a document, or do nothing if it already exists.
938 ///
939 /// A name that was removed comes back empty, keeping its removal floor,
940 /// so the old contents cannot arrive from a replica that never heard
941 /// about the removal.
942 pub fn data_create_doc(&mut self, space: &str, doc: &str) -> Result<()> {
943 Self::validate_ids(space, doc)?;
944 let storage = self.require_data_storage()?;
945 self.load_space(storage.as_ref(), space)?;
946 self.revive_doc(storage.as_ref(), space, doc)?;
947 self.open_doc(storage.as_ref(), space, doc)?;
948 self.persist_space(storage.as_ref(), space)
949 }
950
951 // ---- replication seams ------------------------------------------
952 //
953 // The sync module owns the frames and the peer bookkeeping; these are
954 // the four things it needs from the store, and they are here rather
955 // than there so nothing outside this module touches a document handle.
956
957 /// The backend documents live in, when the layer is on.
958 ///
959 /// Unlike `require_data_storage` this answers `None` rather than an
960 /// error: the sync path treats a switched-off layer as "nothing to do",
961 /// not as a failure to report.
962 pub(crate) fn data_storage_for_sync(&self) -> Option<Arc<dyn ProtocolStateStorage>> {
963 if !self.config.data.enabled {
964 return None;
965 }
966 self.data_storage()
967 }
968
969 /// Every document in a space with the version we hold of it.
970 ///
971 /// A document whose version cannot be read is left out rather than
972 /// failing the whole space. Propagating the error costs every other
973 /// document in the space its replication, and does it silently: one
974 /// unreadable record on either leg would leave the offer empty and both
975 /// replicas believing they had nothing to say to each other. Leaving it
976 /// out instead means the peer reads that document as one we have never
977 /// seen and offers it back, which is the recovery this layer already
978 /// has, and the rest of the space keeps converging meanwhile.
979 pub(crate) fn data_sync_versions(&mut self, space: &str) -> Result<BTreeMap<String, String>> {
980 let docs = self.data_list_docs(space)?;
981 let mut versions = BTreeMap::new();
982 for doc in docs {
983 match self.data_doc_version(space, &doc) {
984 Ok(encoded) => {
985 versions.insert(doc, encoded);
986 }
987 Err(err) => {
988 warn!(space, doc, error = %err, "Leaving a document out of the version offer: its version cannot be read");
989 }
990 }
991 }
992 Ok(versions)
993 }
994
995 /// Every removal this space knows about, with the floor it recorded.
996 ///
997 /// Names that came back are included, because the floor still describes
998 /// content that was removed: a third replica that never heard about the
999 /// removal is holding exactly that content, and this is what tells it
1000 /// to drop it.
1001 pub(crate) fn data_sync_tombstones(&mut self, space: &str) -> Result<BTreeMap<String, String>> {
1002 let Some(storage) = self.data_storage_for_sync() else {
1003 return Ok(BTreeMap::new());
1004 };
1005 offline_protocol_data::validate_space_name(space).map_err(map_data_error)?;
1006 self.load_space(storage.as_ref(), space)?;
1007 Ok(self
1008 .data
1009 .meta
1010 .range((space.to_string(), String::new())..)
1011 .take_while(|((known, _), _)| known == space)
1012 .map(|((_, doc), meta)| (doc.clone(), meta.gone.clone()))
1013 .collect())
1014 }
1015
1016 /// Apply a removal a peer told us about.
1017 ///
1018 /// The floor decides, never the message: a copy the floor covers is
1019 /// removed, and a copy carrying anything beyond it is kept, because that
1020 /// is an edit the removal never saw. Either way the floor is recorded,
1021 /// including for a name this device has never held, so a third replica
1022 /// still holding the removed content cannot seed it back later.
1023 ///
1024 /// A floor this device already holds, or one a held floor covers, is
1025 /// answered from memory with no write and no open. Floors are carried on
1026 /// every offer for the life of the space, so that is the common case,
1027 /// and it has to be free for two reasons. The obvious one is cost: one
1028 /// sealed record per removed name per exchange, from every peer. The
1029 /// other is a name that was brought back on purpose and not yet written
1030 /// to: every version covers the empty one, so a floor that was already
1031 /// judged would read the fresh document as removed content and delete
1032 /// it. A floor decides once, when it first arrives.
1033 pub(crate) fn data_apply_tombstone(
1034 &mut self,
1035 space: &str,
1036 doc: &str,
1037 floor: &VersionToken,
1038 ) -> Result<TombstoneOutcome> {
1039 use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
1040
1041 Self::validate_ids(space, doc)?;
1042 // Checked once on the way in. A floor that does not decode is stored
1043 // for the life of the space and covers nothing, which costs a record
1044 // for no removal at all.
1045 floor.validate().map_err(map_data_error)?;
1046 let storage = self.require_data_storage()?;
1047 self.load_space(storage.as_ref(), space)?;
1048
1049 let held = self
1050 .data
1051 .spaces
1052 .get(space)
1053 .is_some_and(|record| record.docs.contains(doc));
1054
1055 let merged = match self.doc_floor(space, doc) {
1056 Some(existing) if existing.includes(floor).map_err(map_data_error)? => {
1057 return Ok(TombstoneOutcome::Unchanged);
1058 }
1059 Some(existing) => existing.union(floor).map_err(map_data_error)?,
1060 None => floor.clone(),
1061 };
1062 // The union only ever grows, and a peer can grow it by naming a
1063 // fresh replica in every offer. Bounded here rather than at the
1064 // frame, because the frame bounds one floor and this bounds the
1065 // sum of every floor ever merged into this name.
1066 if merged.as_bytes().len() > MAX_REMOVAL_FLOOR_BYTES {
1067 return Err(Error::Other(format!(
1068 "removal floor for {space}/{doc} would exceed {MAX_REMOVAL_FLOOR_BYTES} bytes"
1069 )));
1070 }
1071 let encoded = BASE64.encode(merged.as_bytes());
1072
1073 if !held {
1074 let alive = self
1075 .data
1076 .meta
1077 .get(&(space.to_string(), doc.to_string()))
1078 .is_some_and(|meta| meta.alive);
1079 self.set_doc_meta(
1080 storage.as_ref(),
1081 space,
1082 doc,
1083 DocMeta {
1084 gone: encoded,
1085 alive,
1086 },
1087 )?;
1088 return Ok(TombstoneOutcome::Recorded);
1089 }
1090
1091 // Pending edits count as ours and are flushed before the comparison,
1092 // or an edit made a moment ago would read as content the removal
1093 // covered and be deleted by it.
1094 if let Err(err) = self.data_flush(space, doc) {
1095 debug!(space, doc, error = %err, "Could not flush before judging a removal; judging the last durable version");
1096 }
1097 let ours =
1098 VersionToken::from_bytes(BASE64.decode(self.data_doc_version(space, doc)?).map_err(
1099 |err| Error::Serialization(format!("undecodable document version: {err}")),
1100 )?);
1101
1102 if !merged.includes(&ours).map_err(map_data_error)? {
1103 // Beyond the floor: an edit the removal never saw. The document
1104 // stays, the floor is kept, and the peer gets the edit back
1105 // through the ordinary exchange.
1106 self.set_doc_meta(
1107 storage.as_ref(),
1108 space,
1109 doc,
1110 DocMeta {
1111 gone: encoded,
1112 alive: true,
1113 },
1114 )?;
1115 return Ok(TombstoneOutcome::Kept);
1116 }
1117
1118 self.set_doc_meta(
1119 storage.as_ref(),
1120 space,
1121 doc,
1122 DocMeta {
1123 gone: encoded,
1124 alive: false,
1125 },
1126 )?;
1127
1128 self.data.docs.remove(&(space.to_string(), doc.to_string()));
1129 let first_error = self.purge_doc_records(storage.as_ref(), space, doc, None);
1130 if let Some(record) = self.data.spaces.get_mut(space) {
1131 record.docs.remove(doc);
1132 }
1133 self.persist_space(storage.as_ref(), space)?;
1134 if let Some(detail) = first_error {
1135 warn!(
1136 space,
1137 doc,
1138 detail,
1139 "Removed a document a peer removed, but not every record could be deleted"
1140 );
1141 }
1142
1143 self.emit_event(Event::DataDocRemoved {
1144 space_id: space.to_string(),
1145 doc_id: doc.to_string(),
1146 by: DocRemovedBy::Peer,
1147 });
1148 Ok(TombstoneOutcome::Deleted)
1149 }
1150
1151 /// The documents this device wants from its peers in one space.
1152 ///
1153 /// `patterns` is a list of document names, each of which MAY end in `*`
1154 /// to match a prefix. `["*"]` is everything and is the default; `[]` is
1155 /// nothing. The document charset excludes `*`, so the wildcard can never
1156 /// be part of a name it is matching.
1157 ///
1158 /// Two halves, and only one of them is a request. Toward a peer this is
1159 /// carried in every version offer, so the peer sends nothing else: that
1160 /// is the traffic saving, and it depends on the peer reading the field.
1161 /// Locally it is a refusal, so a document outside it is never created
1162 /// from an offer and never imported however it arrives. The refusal is
1163 /// what makes this safe against a peer that ignores the request, which
1164 /// includes every peer on a build that predates it.
1165 ///
1166 /// Narrowing does not delete what is already held. A document that falls
1167 /// outside the new patterns stops being updated and stays where it is,
1168 /// because deleting on a policy change would make a narrowing lose data
1169 /// no caller asked to lose. `data_delete_doc` is how it leaves.
1170 ///
1171 /// Widening asks: the newly wanted documents are absent from this
1172 /// device's next offer and inside its declared want, so the peer answers
1173 /// with them.
1174 ///
1175 /// The question that asks is skipped in the two cases where it would
1176 /// cost something and buy nothing, because both are the ordinary way an
1177 /// application calls this. Declaring the same patterns again asks for
1178 /// nothing new: the offer would carry exactly what the last one did.
1179 /// And declaring anything at all before `start()`, which is the
1180 /// documented order, cannot reach a peer: the transports are not up, so
1181 /// the frame is dropped for want of a session, and the offer path has
1182 /// already stamped the window that suppresses the next sweep. That stamp
1183 /// would then suppress the start-up sweep and the first rediscovery,
1184 /// delaying the exchange this call exists to bring forward by the length
1185 /// of the window. Both paths already carry the new interest: the sweep
1186 /// in `start` and every rediscovery build their offer from it.
1187 pub fn data_set_interest(&mut self, space: &str, patterns: Vec<String>) -> Result<()> {
1188 offline_protocol_data::validate_space_name(space).map_err(map_data_error)?;
1189 // The same door every other `DataStore` method goes through. Without
1190 // it this is the one call that reports success with the layer off,
1191 // and an application would read that as a narrowing it never got.
1192 self.require_data_storage()?;
1193 if patterns.len() > MAX_INTEREST_PATTERNS {
1194 return Err(Error::InvalidArgument(format!(
1195 "at most {MAX_INTEREST_PATTERNS} interest patterns per space, got {}",
1196 patterns.len()
1197 )));
1198 }
1199 for pattern in &patterns {
1200 validate_interest_pattern(pattern)?;
1201 }
1202 let previous = self.data.interest.get(space).cloned();
1203 let everything = patterns.len() == 1 && patterns[0] == "*";
1204 if everything {
1205 // The default, expressed explicitly. Held as absence so the wire
1206 // and every read below take the cheap path.
1207 self.data.interest.remove(space);
1208 } else {
1209 self.data.interest.insert(space.to_string(), patterns);
1210 }
1211 // Both guards are about the same thing: an offer that would carry
1212 // nothing new, or could not be carried at all, must not spend the
1213 // window that suppresses the offer which can. See the note above.
1214 if previous.as_ref() != self.data.interest.get(space) && self.protocol_is_running() {
1215 self.nudge_data_sync(space, None, "interest_changed");
1216 }
1217 Ok(())
1218 }
1219
1220 /// The patterns declared for a space, or `None` for the default.
1221 pub(crate) fn data_interest(&self, space: &str) -> Option<&Vec<String>> {
1222 self.data.interest.get(space)
1223 }
1224
1225 /// Whether this device wants `doc` in `space`.
1226 pub(crate) fn data_wants(&self, space: &str, doc: &str) -> bool {
1227 match self.data.interest.get(space) {
1228 None => true,
1229 Some(patterns) => patterns
1230 .iter()
1231 .any(|pattern| interest_matches(pattern, doc)),
1232 }
1233 }
1234
1235 /// Let a removed name hold a document again, because something beyond
1236 /// its floor is about to be imported into it.
1237 ///
1238 /// Separate from the refusal, and called only after it: content beyond a
1239 /// removal floor is an edit the removal never saw, and an edit beats a
1240 /// removal. The name comes back keeping its floor, so the *rest* of the
1241 /// removed content still cannot arrive from a replica that has not heard
1242 /// about the removal.
1243 pub(crate) fn data_revive_removed_doc(&mut self, space: &str, doc: &str) -> Result<()> {
1244 let storage = self.require_data_storage()?;
1245 self.load_space(storage.as_ref(), space)?;
1246 self.revive_doc(storage.as_ref(), space, doc)
1247 }
1248
1249 /// The removal floor for a document, with its space loaded first.
1250 ///
1251 /// [`Self::doc_floor`] answers off memory and needs the space loaded.
1252 /// The sync paths below can be the first thing to touch a space after
1253 /// a launch, on the media road in particular, and a floor missed there
1254 /// is a removed document refilled.
1255 fn loaded_doc_floor(&mut self, space: &str, doc: &str) -> Option<VersionToken> {
1256 let storage = self.data_storage_for_sync()?;
1257 self.load_space(storage.as_ref(), space).ok()?;
1258 self.doc_floor(space, doc)
1259 }
1260
1261 /// Whether a document has a removal floor at all.
1262 ///
1263 /// What decides whether a blob for it has to be judged against one.
1264 /// That judgement is an engine decode and has to run inside an
1265 /// in-flight marker, so a name with no floor, which is every name in a
1266 /// space nobody has removed anything from, skips the marker and the
1267 /// decode both.
1268 pub(crate) fn data_doc_has_floor(&mut self, space: &str, doc: &str) -> bool {
1269 self.loaded_doc_floor(space, doc).is_some()
1270 }
1271
1272 /// Whether a version a peer offers is content a removal already covered.
1273 ///
1274 /// Cheap and answered from memory: the overwhelming case is a space with
1275 /// no removals at all, where there is nothing to decode.
1276 pub(crate) fn data_version_below_floor(
1277 &mut self,
1278 space: &str,
1279 doc: &str,
1280 theirs: &VersionToken,
1281 ) -> bool {
1282 let Some(floor) = self.loaded_doc_floor(space, doc) else {
1283 return false;
1284 };
1285 floor.includes(theirs).unwrap_or(false)
1286 }
1287
1288 /// Whether a blob carries nothing beyond a removal floor.
1289 ///
1290 /// An engine decode, not a header read: the blob's changes are decoded
1291 /// into a scratch document to find the version they end at, with the
1292 /// same decoder an import runs. The caller owes it the containment an
1293 /// import gets, an in-flight marker written before and cleared after;
1294 /// see [`offline_protocol_data::blob_end_version`]. A document with no
1295 /// floor skips the decode entirely, which is every document in a space
1296 /// nobody has removed anything from.
1297 pub(crate) fn data_blob_below_floor(&mut self, space: &str, doc: &str, blob: &[u8]) -> bool {
1298 let Some(floor) = self.loaded_doc_floor(space, doc) else {
1299 return false;
1300 };
1301 match offline_protocol_data::blob_end_version(blob) {
1302 Ok(end) => floor.includes(&end).unwrap_or(false),
1303 Err(err) => {
1304 // Undecodable bytes are refused a few steps later by the
1305 // import itself, which reports properly. Not below the
1306 // floor, because nothing here established that.
1307 debug!(space, doc, error = %err, "Could not read a blob's version to judge it against a removal");
1308 false
1309 }
1310 }
1311 }
1312
1313 /// The version we hold of one document, encoded as a frame carries it.
1314 pub(crate) fn data_doc_version(&mut self, space: &str, doc: &str) -> Result<String> {
1315 use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
1316
1317 let token = self.read_doc(space, doc, |document| Ok(document.version()))?;
1318 Ok(BASE64.encode(token.as_bytes()))
1319 }
1320
1321 /// The names a space holds, or `None` if there is nothing to read.
1322 ///
1323 /// Answered off the space index, which `load_space` has already
1324 /// reconciled against the records themselves and cached. Opening a
1325 /// document to find out would defeat both callers, whose whole question
1326 /// is whether opening one is allowed.
1327 fn space_docs(&mut self, space: &str) -> Option<&BTreeSet<String>> {
1328 if offline_protocol_data::validate_space_name(space).is_err() {
1329 return None;
1330 }
1331 let storage = self.data_storage_for_sync()?;
1332 self.load_space(storage.as_ref(), space).ok()?;
1333 self.data.spaces.get(space).map(|record| &record.docs)
1334 }
1335
1336 /// Whether `space` already holds `doc`.
1337 pub(crate) fn data_holds_doc(&mut self, space: &str, doc: &str) -> bool {
1338 self.space_docs(space)
1339 .is_some_and(|docs| docs.contains(doc))
1340 }
1341
1342 /// Whether a document a peer named may be stored in `space`.
1343 ///
1344 /// One already held always may; a new one only while the space is under
1345 /// `cap`. The cap is a parameter rather than a constant here because it
1346 /// bounds what a *peer* can talk this device into storing, which is a
1347 /// replication policy: an application creating its own documents is not
1348 /// subject to it.
1349 ///
1350 /// A space that cannot be read admits, rather than refusing on a storage
1351 /// hiccup: whatever follows fails on the same storage and reports it
1352 /// where the failure actually is.
1353 /// Removed names count against the cap alongside held ones. Each one is
1354 /// a record a peer talked this device into storing, and the record
1355 /// outlives the document by design, so counting only what is held would
1356 /// let a peer name a fresh thousand documents after every removal
1357 /// sweep.
1358 ///
1359 /// A name already known here, held or removed, always admits: nothing a
1360 /// peer says about it costs a record that does not already exist. That
1361 /// matters at the ceiling, where a space stays for good once it gets
1362 /// there, because floors never expire. Refusing a known name there
1363 /// would refuse the floor a peer re-sends on every offer, and a removal
1364 /// for a held document sorted after it.
1365 pub(crate) fn data_space_admits_doc(&mut self, space: &str, doc: &str, cap: usize) -> bool {
1366 // Called once per name in an inbound frame, so it reads both fields
1367 // in place rather than cloning the index: a peer may send offers as
1368 // fast as the link carries them, and this is one of the things each
1369 // one pays for.
1370 if self.space_docs(space).is_none() {
1371 return true;
1372 }
1373 let Some(record) = self.data.spaces.get(space) else {
1374 return true;
1375 };
1376 if record.docs.contains(doc)
1377 || self
1378 .data
1379 .meta
1380 .contains_key(&(space.to_string(), doc.to_string()))
1381 {
1382 return true;
1383 }
1384 let removed = self
1385 .data
1386 .meta
1387 .range((space.to_string(), String::new())..)
1388 .take_while(|((known, _), _)| known == space)
1389 .filter(|((_, name), _)| !record.docs.contains(name.as_str()))
1390 .count();
1391 record.docs.len() + removed < cap
1392 }
1393
1394 /// What a replica at `theirs` is missing from a document.
1395 pub(crate) fn data_catch_up(
1396 &mut self,
1397 space: &str,
1398 doc: &str,
1399 theirs: &VersionToken,
1400 ) -> Result<CatchUp> {
1401 self.read_doc(space, doc, |document| document.export_since(theirs))
1402 }
1403
1404 /// A self-contained encoding of a document, for a peer no run of
1405 /// changes can catch up.
1406 pub(crate) fn data_export_snapshot(&mut self, space: &str, doc: &str) -> Result<Vec<u8>> {
1407 self.read_doc(space, doc, |document| document.export_raw())
1408 }
1409
1410 /// Apply a blob that arrived from a peer, and persist what it changed.
1411 ///
1412 /// The import goes through the engine's remote path, which judges the
1413 /// blob before touching it. A change that applies is flushed here rather
1414 /// than left in memory: an unflushed remote change would be lost to a
1415 /// restart, and the sender has already been told (by the acknowledgement
1416 /// the frame rides on) that it arrived.
1417 ///
1418 /// `Err` means the change is not durable. A document held over its cap
1419 /// still answers `Applied`, because the cap refuses growth *after* the
1420 /// change is on disk; the caller cannot tell the two apart from an error
1421 /// alone, and one of them is a change it must not report as refused.
1422 pub(crate) fn data_apply_remote(
1423 &mut self,
1424 space: &str,
1425 doc: &str,
1426 blob: &[u8],
1427 ) -> Result<RemoteImport> {
1428 Self::validate_ids(space, doc)?;
1429 let storage = self.require_data_storage()?;
1430 self.open_doc(storage.as_ref(), space, doc)?;
1431
1432 let map_key = (space.to_string(), doc.to_string());
1433 let outcome = {
1434 let entry = self
1435 .data
1436 .docs
1437 .get_mut(&map_key)
1438 .ok_or_else(|| Error::Other("document vanished after open".to_string()))?;
1439 let outcome = entry.doc.import_remote(blob).map_err(map_data_error)?;
1440 // A parked change is invisible to the engine's own version, so
1441 // nothing downstream may fold history while one is outstanding:
1442 // compaction would write a snapshot without it and then delete
1443 // the records it is waiting for.
1444 //
1445 // It stays false for the rest of this document's time in memory,
1446 // deliberately. The engine reports what one import left pending,
1447 // never whether a document still holds anything parked, so a
1448 // later import that applies cleanly is not evidence that the
1449 // earlier gap closed. Clearing it on that evidence would re-arm
1450 // compaction while a change is still waiting, which is the loss
1451 // this flag exists to prevent; leaving it set costs a document
1452 // its compaction until it is next opened, where the flag is
1453 // recomputed from the records themselves.
1454 if outcome == RemoteImport::Parked {
1455 entry.history_complete = false;
1456 }
1457 outcome
1458 };
1459
1460 if outcome == RemoteImport::Applied {
1461 // Remote work is not this replica's to push onward, so the flush
1462 // is told where the change came from.
1463 //
1464 // `DocTooLarge` is not an import failure and is not reported as
1465 // one. It is raised after the delta record was written and the
1466 // push decided, so the imported change is already durable; what
1467 // failed is the size verdict that follows, and what it refuses is
1468 // further *growth*. Propagating it would say the change was
1469 // refused when it applied, skip the space record below, and cost
1470 // the caller the one signal that a local edit may have been
1471 // folded into this import and suppressed with it, because that
1472 // compensation is gated on hearing `Applied`. A document over its
1473 // cap is exactly where that matters: the pending edit it still
1474 // accepts is a deletion, which is the route back under.
1475 match self.flush_doc_from(storage.as_ref(), space, doc, Some(space)) {
1476 Ok(()) => {}
1477 Err(Error::DocTooLarge { .. }) => {
1478 debug!(
1479 space,
1480 doc, "Applied a remote change into a document that is over its cap"
1481 );
1482 }
1483 Err(err) => return Err(err),
1484 }
1485 self.persist_space(storage.as_ref(), space)?;
1486 }
1487 Ok(outcome)
1488 }
1489
1490 /// Drop this device's copy of a document, leaving the peers' copies
1491 /// alone.
1492 ///
1493 /// Eviction, not removal: it reclaims the space a document occupies here
1494 /// and records nothing about the name, so the next version exchange with
1495 /// a replica that still holds it recreates and refills it. That is the
1496 /// right call for a cache and the wrong one for content somebody wants
1497 /// gone from the room, which is what [`Self::data_remove_doc`] is for.
1498 pub fn data_delete_doc(&mut self, space: &str, doc: &str) -> Result<()> {
1499 Self::validate_ids(space, doc)?;
1500 let storage = self.require_data_storage()?;
1501 self.load_space(storage.as_ref(), space)?;
1502
1503 self.data.docs.remove(&(space.to_string(), doc.to_string()));
1504 let first_error = self.purge_doc_records(storage.as_ref(), space, doc, None);
1505 if let Some(record) = self.data.spaces.get_mut(space) {
1506 record.docs.remove(doc);
1507 }
1508 self.persist_space(storage.as_ref(), space)?;
1509
1510 match first_error {
1511 Some(detail) => Err(Error::Other(format!(
1512 "failed to delete every record of document {space}/{doc}: {detail}"
1513 ))),
1514 None => Ok(()),
1515 }
1516 }
1517
1518 /// Remove a document from every replica of its space.
1519 ///
1520 /// Records the version the document stood at as its removal floor, then
1521 /// deletes it here and tells the space. A replica whose copy the floor
1522 /// covers deletes it too; a replica holding an edit the floor does not
1523 /// cover keeps its copy and hands the document back, contents and all,
1524 /// because that edit's history *is* those contents. An edit therefore
1525 /// beats a removal, whichever happened first, and the pair never
1526 /// produces a half-emptied document.
1527 ///
1528 /// Pending edits here are flushed first so the floor covers them:
1529 /// removing a document must not leave this device's own unsent work
1530 /// looking like somebody else's concurrent edit.
1531 ///
1532 /// Removing a name this device does not hold does nothing. There is no
1533 /// version to take a floor from, and an empty floor would name no
1534 /// content at all.
1535 ///
1536 /// Re-using the name afterwards starts a document whose history is
1537 /// disjoint from the floor. What it does not do is fence the old
1538 /// contents off: a replica that edited them concurrently keeps them and
1539 /// the ordinary exchange merges them into the new document, and a
1540 /// replica that never learned of the removal merges the new contents
1541 /// into its old copy. A name is cheap; a fresh one is the safe choice.
1542 pub fn data_remove_doc(&mut self, space: &str, doc: &str) -> Result<()> {
1543 Self::validate_ids(space, doc)?;
1544 let storage = self.require_data_storage()?;
1545 self.load_space(storage.as_ref(), space)?;
1546 if !self.removable(space, doc) {
1547 return Ok(());
1548 }
1549
1550 // Errors here are logged and stepped over, exactly as the import
1551 // path's pre-flush does. A document over its cap fails every flush
1552 // and must still be removable; refusing would leave the only way out
1553 // of the cap closed. The floor is taken from the version held in
1554 // memory either way, which already includes what the flush could
1555 // not write.
1556 if let Err(err) = self.data_flush(space, doc) {
1557 debug!(space, doc, error = %err, "Could not flush before removing; the floor is taken from the version held in memory");
1558 }
1559
1560 let first_error = self.record_removal(storage.as_ref(), space, doc, None)?;
1561 self.persist_space(storage.as_ref(), space)?;
1562 self.nudge_data_sync(space, None, "document_removed");
1563
1564 match first_error {
1565 Some(detail) => Err(Error::Other(format!(
1566 "document {space}/{doc} is removed, but not every record could be deleted: \
1567 {detail}"
1568 ))),
1569 None => Ok(()),
1570 }
1571 }
1572
1573 /// Whether a removal has anything to act on: the document is held here
1574 /// and does not already stand removed.
1575 ///
1576 /// Removing a name this device does not hold does nothing. There is no
1577 /// version to take a floor from, and an empty floor would name no
1578 /// content at all. The space must already be loaded.
1579 fn removable(&self, space: &str, doc: &str) -> bool {
1580 if self.doc_is_removed(space, doc) {
1581 return false;
1582 }
1583 let held = self
1584 .data
1585 .spaces
1586 .get(space)
1587 .is_some_and(|record| record.docs.contains(doc));
1588 if !held {
1589 debug!(
1590 space,
1591 doc, "Nothing to remove: this device does not hold the document"
1592 );
1593 }
1594 held
1595 }
1596
1597 /// Record a document's removal floor, drop the copy held here and purge
1598 /// its records, reporting the first record that would not delete.
1599 ///
1600 /// What one removal writes, without what a batch of them shares: the
1601 /// space index is not persisted here and the space is not told, so a
1602 /// caller removing every document in a space does each once at the end
1603 /// rather than once per document.
1604 ///
1605 /// The floor is written before the records it removes. A crash in
1606 /// between leaves a name every reader already treats as removed, which
1607 /// the next open finishes; the reverse order leaves records that vote
1608 /// the document back into existence.
1609 ///
1610 /// `delta_keys` is the delta log's listing when the caller already has
1611 /// one. It has to postdate the document's last flush, or the delta that
1612 /// flush wrote is missed by the purge and replayed into the next
1613 /// document of the same name.
1614 fn record_removal(
1615 &mut self,
1616 storage: &dyn ProtocolStateStorage,
1617 space: &str,
1618 doc: &str,
1619 delta_keys: Option<&[String]>,
1620 ) -> Result<Option<String>> {
1621 let floor = self.data_doc_version(space, doc)?;
1622 let gone = match self.doc_floor(space, doc) {
1623 // Two removals of one name, ours and one we learned from a
1624 // peer, each cover changes the other does not.
1625 Some(existing) => {
1626 use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
1627 let taken = VersionToken::from_bytes(BASE64.decode(&floor).map_err(|err| {
1628 Error::Serialization(format!("undecodable document version: {err}"))
1629 })?);
1630 BASE64.encode(existing.union(&taken).map_err(map_data_error)?.as_bytes())
1631 }
1632 None => floor,
1633 };
1634 self.set_doc_meta(storage, space, doc, DocMeta { gone, alive: false })?;
1635
1636 self.data.docs.remove(&(space.to_string(), doc.to_string()));
1637 let first_error = self.purge_doc_records(storage, space, doc, delta_keys);
1638 if let Some(record) = self.data.spaces.get_mut(space) {
1639 record.docs.remove(doc);
1640 }
1641
1642 self.emit_event(Event::DataDocRemoved {
1643 space_id: space.to_string(),
1644 doc_id: doc.to_string(),
1645 by: DocRemovedBy::Local,
1646 });
1647 Ok(first_error)
1648 }
1649
1650 /// Remove every document a space holds, from every replica of it.
1651 ///
1652 /// Each document is attempted even after one fails, and the first error
1653 /// is what the caller hears. What every removal shares is done once for
1654 /// the lot: the delta log is listed once, the space index is written
1655 /// once, and the space is told once, with one offer carrying every
1656 /// removal. Per document that is two full listings and one sealed index
1657 /// write fewer, which on a custom backend is three round trips under the
1658 /// engine lock, for each of up to a thousand names.
1659 pub fn data_remove_space(&mut self, space: &str) -> Result<()> {
1660 offline_protocol_data::validate_space_name(space).map_err(map_data_error)?;
1661 let storage = self.require_data_storage()?;
1662 self.load_space(storage.as_ref(), space)?;
1663 let docs: Vec<String> = self
1664 .data_list_docs(space)?
1665 .into_iter()
1666 .filter(|doc| self.removable(space, doc))
1667 .collect();
1668 if docs.is_empty() {
1669 return Ok(());
1670 }
1671
1672 // Every document is flushed before the delta log is listed, and
1673 // nothing writes to the log between the listing and the purges. A
1674 // flush after the listing would write a delta the listing does not
1675 // name, the purge would leave it behind, and the next document under
1676 // that name would replay it: the removed contents, back.
1677 for doc in &docs {
1678 if let Err(err) = self.data_flush(space, doc) {
1679 debug!(space, doc, error = %err, "Could not flush before removing; the floor is taken from the version held in memory");
1680 }
1681 }
1682 let delta_keys = match storage.list_keys(storage_keys::DATA_DELTA_LOG) {
1683 Ok(keys) => Some(keys),
1684 Err(err) => {
1685 // Each removal lists for itself, as a single one does.
1686 warn!(space, error = %err, "Could not list the delta log once for the space; listing per document");
1687 None
1688 }
1689 };
1690
1691 let mut first_error: Option<Error> = None;
1692 let mut note = |err: Error| {
1693 if first_error.is_none() {
1694 first_error = Some(err);
1695 }
1696 };
1697 for doc in &docs {
1698 match self.record_removal(storage.as_ref(), space, doc, delta_keys.as_deref()) {
1699 Ok(None) => {}
1700 Ok(Some(detail)) => note(Error::Other(format!(
1701 "document {space}/{doc} is removed, but not every record could be deleted: \
1702 {detail}"
1703 ))),
1704 Err(err) => note(err),
1705 }
1706 }
1707
1708 // The index once, at the end. A crash before it leaves an index
1709 // that still lists the removed names, and the next open reads the
1710 // removal records after it and drops them: the index is a cache,
1711 // and the removal record is what decides.
1712 if let Err(err) = self.persist_space(storage.as_ref(), space) {
1713 note(err);
1714 }
1715 // Told regardless of errors. An error past a removal record is
1716 // still a removal, and one before it costs at worst an offer that
1717 // says nothing new.
1718 self.nudge_data_sync(space, None, "space_removed");
1719
1720 match first_error {
1721 Some(err) => Err(err),
1722 None => Ok(()),
1723 }
1724 }
1725
1726 /// The documents in a space.
1727 pub fn data_list_docs(&mut self, space: &str) -> Result<Vec<String>> {
1728 offline_protocol_data::validate_space_name(space).map_err(map_data_error)?;
1729 let storage = self.require_data_storage()?;
1730 self.load_space(storage.as_ref(), space)?;
1731 Ok(self
1732 .data
1733 .spaces
1734 .get(space)
1735 .map(|record| record.docs.iter().cloned().collect())
1736 .unwrap_or_default())
1737 }
1738
1739 /// Every space the store knows of.
1740 ///
1741 /// Derived from what is on disk, so a space whose last document was
1742 /// deleted still lists while its index record survives, and a space
1743 /// whose documents were all removed lists for as long as its removal
1744 /// records do, which is the life of the space.
1745 pub fn data_list_spaces(&mut self) -> Result<Vec<String>> {
1746 let storage = self.require_data_storage()?;
1747 let mut spaces = BTreeSet::new();
1748 // Removal records count: a space whose documents were all removed
1749 // still has something to say to its peers, and leaving it out of
1750 // this list is what would stop the start-up sweep telling them.
1751 for key_type in [
1752 storage_keys::DATA_SPACES,
1753 storage_keys::DATA_DOCS,
1754 storage_keys::DATA_DELTA_LOG,
1755 storage_keys::DATA_DOC_META,
1756 ] {
1757 if let Ok(keys) = storage.list_keys(key_type) {
1758 for key in keys {
1759 let name = key.split('/').next().unwrap_or(&key);
1760 if !name.is_empty() {
1761 spaces.insert(name.to_string());
1762 }
1763 }
1764 }
1765 }
1766 for space in self.data.spaces.keys() {
1767 spaces.insert(space.clone());
1768 }
1769 Ok(spaces.into_iter().collect())
1770 }
1771
1772 /// Set a key in a map collection.
1773 pub fn data_map_set(
1774 &mut self,
1775 space: &str,
1776 doc: &str,
1777 collection: &str,
1778 key: &str,
1779 value: DataValue,
1780 ) -> Result<()> {
1781 self.with_doc(space, doc, true, |document| {
1782 document.map_set(collection, key, value)
1783 })
1784 }
1785
1786 /// Remove a key from a map collection.
1787 pub fn data_map_delete(
1788 &mut self,
1789 space: &str,
1790 doc: &str,
1791 collection: &str,
1792 key: &str,
1793 ) -> Result<()> {
1794 self.with_doc(space, doc, false, |document| {
1795 document.map_delete(collection, key)
1796 })
1797 }
1798
1799 /// Read a key from a map collection.
1800 pub fn data_map_get(
1801 &mut self,
1802 space: &str,
1803 doc: &str,
1804 collection: &str,
1805 key: &str,
1806 ) -> Result<Option<DataValue>> {
1807 self.read_doc(space, doc, |document| document.map_get(collection, key))
1808 }
1809
1810 /// Append to a list collection.
1811 pub fn data_list_push(
1812 &mut self,
1813 space: &str,
1814 doc: &str,
1815 collection: &str,
1816 value: DataValue,
1817 ) -> Result<()> {
1818 self.with_doc(space, doc, true, |document| {
1819 document.list_push(collection, value)
1820 })
1821 }
1822
1823 /// Insert into a list collection.
1824 pub fn data_list_insert(
1825 &mut self,
1826 space: &str,
1827 doc: &str,
1828 collection: &str,
1829 index: u32,
1830 value: DataValue,
1831 ) -> Result<()> {
1832 self.with_doc(space, doc, true, |document| {
1833 document.list_insert(collection, index as usize, value)
1834 })
1835 }
1836
1837 /// Delete entries from a list collection.
1838 pub fn data_list_delete(
1839 &mut self,
1840 space: &str,
1841 doc: &str,
1842 collection: &str,
1843 index: u32,
1844 count: u32,
1845 ) -> Result<()> {
1846 self.with_doc(space, doc, false, |document| {
1847 document.list_delete(collection, index as usize, count as usize)
1848 })
1849 }
1850
1851 /// The number of entries in a list collection.
1852 pub fn data_list_len(&mut self, space: &str, doc: &str, collection: &str) -> Result<u32> {
1853 let len = self.read_doc(space, doc, |document| document.list_len(collection))?;
1854 Ok(len as u32)
1855 }
1856
1857 /// Insert into a text collection at a character position.
1858 pub fn data_text_insert(
1859 &mut self,
1860 space: &str,
1861 doc: &str,
1862 collection: &str,
1863 position: u32,
1864 text: &str,
1865 ) -> Result<()> {
1866 self.with_doc(space, doc, true, |document| {
1867 document.text_insert(collection, position as usize, text)
1868 })
1869 }
1870
1871 /// Delete characters from a text collection.
1872 pub fn data_text_delete(
1873 &mut self,
1874 space: &str,
1875 doc: &str,
1876 collection: &str,
1877 position: u32,
1878 count: u32,
1879 ) -> Result<()> {
1880 self.with_doc(space, doc, false, |document| {
1881 document.text_delete(collection, position as usize, count as usize)
1882 })
1883 }
1884
1885 /// The contents of a text collection.
1886 pub fn data_text_value(&mut self, space: &str, doc: &str, collection: &str) -> Result<String> {
1887 self.read_doc(space, doc, |document| document.text_value(collection))
1888 }
1889
1890 /// Add to a counter collection. Negative values subtract.
1891 pub fn data_counter_increment(
1892 &mut self,
1893 space: &str,
1894 doc: &str,
1895 collection: &str,
1896 amount: f64,
1897 ) -> Result<()> {
1898 self.with_doc(space, doc, true, |document| {
1899 document.counter_increment(collection, amount)
1900 })
1901 }
1902
1903 /// The value of a counter collection.
1904 pub fn data_counter_value(&mut self, space: &str, doc: &str, collection: &str) -> Result<f64> {
1905 self.read_doc(space, doc, |document| document.counter_value(collection))
1906 }
1907
1908 /// The document's current state as plain JSON.
1909 pub fn data_doc_json(&mut self, space: &str, doc: &str) -> Result<String> {
1910 self.read_doc(space, doc, |document| document.export_json())
1911 }
1912
1913 /// The document's full history, engine-encoded.
1914 ///
1915 /// Half of the escape hatch: this and [`Self::data_doc_json`] are what
1916 /// let an application take its data and leave.
1917 pub fn data_export_raw(&mut self, space: &str, doc: &str) -> Result<Vec<u8>> {
1918 self.read_doc(space, doc, |document| document.export_raw())
1919 }
1920
1921 /// Persist any pending edits to a document.
1922 ///
1923 /// Writes one delta record, then compacts if the log has outgrown the
1924 /// document. The order is load-bearing: the delta is durable before any
1925 /// bookkeeping assumes it, and at compaction the fresh snapshot is
1926 /// durable before the deltas it folded are deleted. A crash at any point
1927 /// leaves duplicate history, which the merge absorbs, rather than a gap,
1928 /// which it cannot.
1929 pub fn data_flush(&mut self, space: &str, doc: &str) -> Result<()> {
1930 Self::validate_ids(space, doc)?;
1931 let storage = self.require_data_storage()?;
1932 self.open_doc(storage.as_ref(), space, doc)?;
1933 self.flush_doc(storage.as_ref(), space, doc)
1934 }
1935
1936 fn flush_doc(
1937 &mut self,
1938 storage: &dyn ProtocolStateStorage,
1939 space: &str,
1940 doc: &str,
1941 ) -> Result<()> {
1942 self.flush_doc_from(storage, space, doc, None)
1943 }
1944
1945 /// [`Self::flush_doc`], told which peer the change came from.
1946 ///
1947 /// `origin` is `None` for a local edit and the peer's address for one
1948 /// applied from a sync frame. It exists so replication does not send a
1949 /// change straight back to whoever sent it: the merge would absorb the
1950 /// echo, but on a mesh it is a frame nobody needed.
1951 fn flush_doc_from(
1952 &mut self,
1953 storage: &dyn ProtocolStateStorage,
1954 space: &str,
1955 doc: &str,
1956 origin: Option<&str>,
1957 ) -> Result<()> {
1958 let map_key = (space.to_string(), doc.to_string());
1959
1960 let Some(entry) = self.data.docs.get_mut(&map_key) else {
1961 return Ok(());
1962 };
1963 let Some(delta) = entry.doc.commit().map_err(map_data_error)? else {
1964 return Ok(());
1965 };
1966 let seq = entry.next_seq;
1967 let delta_bytes = delta.bytes.len();
1968 let history_complete = entry.history_complete;
1969
1970 // A commit too large for one record can never be stored as a delta,
1971 // and refusing it is not a recovery: the rewind below puts the change
1972 // back in the pending set, so the *next* commit exports a strictly
1973 // larger delta and fails the same way forever. A compacted snapshot
1974 // is bounded by the document cap rather than by history, so it is the
1975 // route that still works, and the commit's changes are inside it.
1976 let oversized = delta_bytes > MAX_PROTOCOL_STATE_RECORD_BYTES;
1977
1978 let rewind = |protocol: &mut Self| {
1979 if let Some(entry) = protocol.data.docs.get_mut(&map_key) {
1980 entry.doc.rewind_last_commit();
1981 }
1982 };
1983
1984 if oversized && !history_complete {
1985 // Compaction is the only way out and it is unavailable: folding a
1986 // log with a hole in it would delete records the document still
1987 // needs. Say so rather than looping on an unwritable delta.
1988 rewind(self);
1989 return Err(Error::Other(format!(
1990 "commit for document {space}/{doc} is {delta_bytes} bytes, over the \
1991 {MAX_PROTOCOL_STATE_RECORD_BYTES} byte record limit, and its delta log \
1992 is incomplete so it cannot be compacted instead"
1993 )));
1994 }
1995
1996 if oversized {
1997 warn!(
1998 space,
1999 doc, delta_bytes, "Commit is too large for one delta record; compacting instead"
2000 );
2001 if let Err(err) = self.compact_doc(storage, space, doc) {
2002 rewind(self);
2003 return Err(err);
2004 }
2005 } else if let Err(err) = self.write_state_record(
2006 storage,
2007 storage_keys::DATA_DELTA_LOG,
2008 &delta_key(space, doc, seq),
2009 &delta.bytes,
2010 ) {
2011 // Put the export marker back. The commit already advanced it, so
2012 // leaving it advanced would mean this change is never handed out
2013 // again: the document keeps it in memory, the next commit exports
2014 // only what came after, and the change is simply gone at the next
2015 // restart. Re-exporting it later costs at worst a duplicate
2016 // record, which the merge absorbs.
2017 rewind(self);
2018 return Err(Error::Other(format!(
2019 "failed to persist document delta: {err}"
2020 )));
2021 }
2022
2023 let (verdict, measured, should_compact, compacted_before) = {
2024 let entry = self
2025 .data
2026 .docs
2027 .get_mut(&map_key)
2028 .ok_or_else(|| Error::Other("document vanished during flush".to_string()))?;
2029 if !oversized {
2030 entry.next_seq = seq.saturating_add(1);
2031 entry.log_bytes = entry.log_bytes.saturating_add(delta_bytes);
2032 }
2033
2034 let (verdict, measured) = entry.doc.check_size().map_err(map_data_error)?;
2035 if let Some(bytes) = measured {
2036 entry.compacted_bytes = bytes;
2037 }
2038 // Never fold a log that did not fully apply, and never compact
2039 // twice for one flush.
2040 let should_compact = !oversized
2041 && entry.history_complete
2042 && policy::should_compact(
2043 entry.log_bytes,
2044 entry.compacted_bytes,
2045 entry.doc.commits_since_compaction(),
2046 );
2047 (verdict, measured, should_compact, entry.compacted_bytes)
2048 };
2049
2050 // Replicated here for the same reason the event fires here: the
2051 // claim being made is that the change is durable, and that became
2052 // true at the write above. Pushing before it would advertise a
2053 // change that a crash could still take back, and pushing after
2054 // compaction would let a compaction failure suppress replication of
2055 // something already on disk.
2056 //
2057 // The oversized branch wrote a compacted snapshot instead of a delta
2058 // record, so there is no delta to push. Offering versions is how the
2059 // peer finds the gap: waiting for its next offer would leave both
2060 // replicas believing they agree for as long as the link holds, since
2061 // a link that never drops never fires a trigger.
2062 if oversized {
2063 self.nudge_data_sync(space, origin, "oversized_commit");
2064 } else {
2065 self.push_data_delta(space, doc, &delta.bytes, origin);
2066 }
2067
2068 // Emitted here, before compaction: the event's claim is that the
2069 // change reached storage, and that became true at the write above.
2070 // Emitting after compaction would let a compaction failure suppress
2071 // the notification for a change that is already durable.
2072 self.emit_event(Event::DataChanged {
2073 space_id: space.to_string(),
2074 doc_id: doc.to_string(),
2075 delta_bytes: delta_bytes as u64,
2076 });
2077
2078 if should_compact {
2079 self.compact_doc(storage, space, doc)?;
2080 }
2081
2082 match verdict {
2083 policy::SizeVerdict::TooLarge => {
2084 let actual = measured.unwrap_or(compacted_before);
2085 if let Some(entry) = self.data.docs.get_mut(&map_key) {
2086 entry.over_cap = true;
2087 }
2088 // The change is already durable: refusing to persist it would
2089 // lose work the application believed it had made. What is
2090 // refused is further growth, and deletions stay open so the
2091 // document can be brought back under the cap.
2092 Err(Error::DocTooLarge {
2093 actual,
2094 limit: policy::MAX_DOC_BYTES,
2095 })
2096 }
2097 policy::SizeVerdict::Warn => {
2098 if let Some(entry) = self.data.docs.get_mut(&map_key) {
2099 entry.over_cap = false;
2100 }
2101 self.emit_event(Event::DataDocSizeWarning {
2102 space_id: space.to_string(),
2103 doc_id: doc.to_string(),
2104 compacted_bytes: measured.unwrap_or(compacted_before) as u64,
2105 cap_bytes: policy::MAX_DOC_BYTES as u64,
2106 });
2107 Ok(())
2108 }
2109 policy::SizeVerdict::Ok => {
2110 if let Some(entry) = self.data.docs.get_mut(&map_key) {
2111 entry.over_cap = false;
2112 }
2113 Ok(())
2114 }
2115 }
2116 }
2117
2118 /// Fold a document's delta log into a fresh compacted record.
2119 fn compact_doc(
2120 &mut self,
2121 storage: &dyn ProtocolStateStorage,
2122 space: &str,
2123 doc: &str,
2124 ) -> Result<()> {
2125 let map_key = (space.to_string(), doc.to_string());
2126 let compacted = {
2127 let Some(entry) = self.data.docs.get(&map_key) else {
2128 return Ok(());
2129 };
2130 // Folding a log that did not fully apply would write a snapshot
2131 // missing those changes and then delete the records that still
2132 // hold them, turning a recoverable gap into permanent loss. An
2133 // oversized delta log is the cheaper failure.
2134 if !entry.history_complete {
2135 warn!(
2136 space,
2137 doc, "Skipping compaction: the document's delta log did not fully apply"
2138 );
2139 return Ok(());
2140 }
2141 entry.doc.export_compacted().map_err(map_data_error)?
2142 };
2143
2144 // Durable first. Everything after this point is cleanup, and a crash
2145 // in the middle of cleanup costs disk, not data.
2146 self.write_state_record(
2147 storage,
2148 storage_keys::DATA_DOCS,
2149 &doc_key(space, doc),
2150 &compacted,
2151 )
2152 .map_err(|err| Error::Other(format!("failed to persist compacted document: {err}")))?;
2153
2154 let prefix = format!("{space}/{doc}/");
2155 if let Ok(keys) = storage.list_keys(storage_keys::DATA_DELTA_LOG) {
2156 for key in keys.iter().filter(|key| key.starts_with(&prefix)) {
2157 let _ = storage.delete(storage_keys::DATA_DELTA_LOG, key);
2158 }
2159 }
2160
2161 if let Some(entry) = self.data.docs.get_mut(&map_key) {
2162 entry.log_bytes = 0;
2163 entry.compacted_bytes = compacted.len();
2164 entry.doc.mark_compacted(compacted.len());
2165 }
2166 debug!(space, doc, bytes = compacted.len(), "Compacted document");
2167 Ok(())
2168 }
2169
2170 /// Persist every open document with pending edits.
2171 ///
2172 /// Called on shutdown, so the debounce window between an edit and its
2173 /// record can never be the reason a change is lost.
2174 pub fn data_flush_all(&mut self) -> Result<()> {
2175 let Ok(storage) = self.require_data_storage() else {
2176 return Ok(());
2177 };
2178 let keys: Vec<(String, String)> = self.data.docs.keys().cloned().collect();
2179 for (space, doc) in keys {
2180 match self.flush_doc(storage.as_ref(), &space, &doc) {
2181 Ok(()) => {}
2182 // Not a failed flush, and not reported as one: the record was
2183 // written and only the size verdict after it refused further
2184 // growth. On a shutdown path the distinction is the whole
2185 // question an operator is asking, which is whether anything
2186 // was lost.
2187 Err(Error::DocTooLarge { .. }) => {
2188 debug!(
2189 space,
2190 doc, "Flushed a document that is over its cap on shutdown"
2191 );
2192 }
2193 Err(err) => {
2194 warn!(space, doc, error = %err, "Failed to flush document");
2195 }
2196 }
2197 }
2198 Ok(())
2199 }
2200
2201 /// Delete every record the data layer owns.
2202 ///
2203 /// The logout path for an application that pointed documents at its own
2204 /// backend. `wipePersistedState` clears the account directory of the
2205 /// *default* provider, which a custom backend is not inside, so without
2206 /// this call those documents would outlive the account that made them.
2207 ///
2208 /// **Only durable once replication has stopped.** There are no deletion
2209 /// tombstones in this release, so nothing distinguishes a space this
2210 /// device wiped from one it has never seen. Called while the engine is
2211 /// running with live sessions, every document comes back: the peer's next
2212 /// version offer names them and they are recreated and refilled from the
2213 /// peer's copy, and an offer of our own naming nothing reads as a replica
2214 /// that has never seen the space. On the logout path the engine is being
2215 /// torn down anyway. Anywhere else, stop it first, and only for as long
2216 /// as it stays stopped: the peer still holds the documents, so they return
2217 /// when replication resumes with it. The call clears this device, it does
2218 /// not delete content.
2219 pub fn data_wipe_all(&mut self) -> Result<()> {
2220 let Some(storage) = self.data_storage() else {
2221 return Ok(());
2222 };
2223 self.data.docs.clear();
2224 self.data.spaces.clear();
2225 self.data.meta.clear();
2226 // Interest goes with the content it was scoping, for the reason
2227 // stated above: nothing may distinguish a space this device wiped
2228 // from one it has never seen, and a surviving narrowing is exactly
2229 // such a distinction. It is also the logout path, so a narrowing
2230 // left behind would be one account's policy silently applied to the
2231 // next account that happens to replicate with the same peer.
2232 self.data.interest.clear();
2233 // Every window and every outstanding question goes with the content
2234 // they were about. Left behind, a window suppresses the first offer
2235 // or the first blob request made after the wipe, which is the same
2236 // shape [`Self::forget_data_sync_peer`] exists to prevent: a window
2237 // that survives the thing it was measuring.
2238 //
2239 // The pending fetches are not bookkeeping. An entry there is what
2240 // admits arriving attachment bytes, so one surviving a wipe would let
2241 // bytes land for a space this device no longer has, and hand them to
2242 // the application as an answer to a question the wipe erased.
2243 self.last_data_sync_offer.clear();
2244 self.data_sync_settle.clear();
2245 self.pending_attachment_fetches.clear();
2246 self.blob_request_windows.clear();
2247
2248 // Attempt every record and report the first failure, as
2249 // [`Self::data_delete_doc`] does. Answering `Ok` for a wipe that left
2250 // records behind is the worst shape this call can take: it is the
2251 // logout path, so the records it failed to remove outlive the account
2252 // that made them, and the application has no symptom to notice it by.
2253 let mut first_error: Option<String> = None;
2254
2255 // Custody goes with the documents. A custody store that survived a
2256 // logout would hold other people's traffic past the point the user
2257 // asked for erasure, and there is no global wipe for it to inherit
2258 // (`docs/spec/custody.md`, "Erase"), so this wipe calls its own.
2259 if let Err(err) = self.erase_custody() {
2260 first_error = Some(err.to_string());
2261 }
2262
2263 let mut record_error = |err: crate::protocol_state_storage::ProtocolStateError| {
2264 if first_error.is_none() {
2265 first_error = Some(err.to_string());
2266 }
2267 };
2268
2269 // Every key type the layer owns, replication bookkeeping included.
2270 // A key type missing from this list is not a partial wipe that
2271 // reports itself, it is a silent one: the records are keyed by peer
2272 // address, so what survives a logout is who this account replicated
2273 // with.
2274 for key_type in [
2275 storage_keys::DATA_DOCS,
2276 storage_keys::DATA_DELTA_LOG,
2277 storage_keys::DATA_SPACES,
2278 storage_keys::DATA_SYNC,
2279 storage_keys::DATA_DOC_META,
2280 ] {
2281 match storage.list_keys(key_type) {
2282 Ok(keys) => {
2283 for key in keys {
2284 if let Err(err) = storage.delete(key_type, &key) {
2285 record_error(err);
2286 }
2287 }
2288 }
2289 Err(err) => record_error(err),
2290 }
2291 }
2292
2293 match first_error {
2294 Some(detail) => Err(Error::Other(format!(
2295 "failed to delete every data record: {detail}"
2296 ))),
2297 None => Ok(()),
2298 }
2299 }
2300
2301 /// The compacted size of a document, in bytes.
2302 pub fn data_doc_size(&mut self, space: &str, doc: &str) -> Result<u64> {
2303 Self::validate_ids(space, doc)?;
2304 let storage = self.require_data_storage()?;
2305 self.open_doc(storage.as_ref(), space, doc)?;
2306 let entry = self
2307 .data
2308 .docs
2309 .get_mut(&(space.to_string(), doc.to_string()))
2310 .ok_or_else(|| Error::Other("document vanished after open".to_string()))?;
2311 let bytes = entry.doc.measure().map_err(map_data_error)?;
2312 entry.compacted_bytes = bytes;
2313 Ok(bytes as u64)
2314 }
2315}