Skip to main content

haematite_cli/serve/
changes.rs

1//! `changes.list`: the documents added, changed or removed between two kept
2//! versions of one branch, in (collection, id) order, page by page.
3//!
4//! The changes come from the engine's diff of each shard's two roots over the
5//! revision entries (`m\0`) alone, so no body is read unless values are asked,
6//! and a page costs in proportion to the changes, never to the store. Each
7//! shard's diff resumes after the last key the answer passed, and the shards'
8//! changes are merged in key order, so the sealed `next` holds one key.
9//!
10//! A row's `before` and `after` are the versions its revisions were written
11//! at. A record merged in from another branch keeps the version it was written
12//! at on its own side; the version the change belongs to is the answer's `to`.
13//!
14//! A change to a record the caller may not read is absent and not counted, but
15//! it is taken against the page's limit: a page that reaches its limit inside
16//! a run of such changes gives no rows and a `next`. A caller can learn from
17//! such pages that changes lie there which it was not given, and their number
18//! bounds how many; never which records, or where.
19//!
20//! `to: {branch}` with no version is the head, captured on the first page;
21//! the answer's `to` names it, and every later page reads at that version from
22//! the token, so a writer between pages changes nothing. A live view asks again
23//! with `from` set to the last `to` it was given. `from: {"fork": branch}` is
24//! the moment that branch forked at, from its fork provenance; `to` must then
25//! be on that branch.
26//!
27//! `from` may be later than `to`: the answer is then the reverse, what would
28//! be undone going back, with added and removed exchanged and each row's
29//! `before` and `after` exchanged.
30//!
31//! With values, each body is JSON as in query.run's rows: every number's text
32//! as written, object keys in sorted order. The bytes exactly as stored are
33//! `documents.get`'s `raw_json` at that version.
34
35#[cfg(test)]
36use std::cell::Cell;
37use std::collections::{BTreeMap, VecDeque};
38use std::num::{NonZeroU32, NonZeroU64, NonZeroUsize};
39
40use haematite::db::{ObservedChange, ObserverError, ReadOnlyDatabase};
41use haematite::tree::{DiffBudget, DiffError, DiffInterval};
42use haematite::{Hash, TreeError};
43use haematite_query::page::{Allowance, Origin, Resume};
44use serde::{Deserialize, Serialize};
45use serde_json::value::RawValue;
46use serde_json::{Value, json};
47
48use super::access::Access;
49use super::contracts::Caller;
50use super::protocol::{QueryPage, Selection, Version};
51use super::query::{Binding, Continuation, Engine, Rows, Seal};
52use super::storage::{
53    Captured, Catalogue, DocumentRevision, Failure, Held, document_key, reads, revision_key,
54};
55
56const REVISIONS: &[u8] = b"m\0";
57const REVISIONS_END: &[u8] = b"m\x01";
58
59#[cfg(test)]
60thread_local! {
61    /// Diff calls and body reads made on this thread, for tests.
62    static DIFFS: Cell<u64> = const { Cell::new(0) };
63    static BODIES: Cell<u64> = const { Cell::new(0) };
64}
65
66/// The most changes one shard's diff gives at once.
67const BATCH: usize = 64;
68
69/// The most bytes a collection name or record id may have.
70const ID_BYTES: usize = 512;
71
72/// The named limits of `changes.list`, each set in serve.json or defaulted.
73#[derive(Clone, Copy, Debug)]
74pub struct Settings {
75    /// Changes one page may take, denied ones included; at least 2.
76    pub scan_records: NonZeroU64,
77    /// Tree nodes one shard's diff may load for one call.
78    pub node_budget: NonZeroUsize,
79    /// The most rows a page may ask for.
80    pub max_page: NonZeroU32,
81}
82
83impl Default for Settings {
84    fn default() -> Self {
85        Self {
86            scan_records: NonZeroU64::MIN.saturating_add(9_999),
87            // As `branch.review`'s node budget (storage/review.rs).
88            node_budget: NonZeroUsize::MIN.saturating_add(65_535),
89            max_page: NonZeroU32::MIN.saturating_add(999),
90        }
91    }
92}
93
94/// A moment a caller names: a branch at a version, or its head (`to` only),
95/// or the moment a branch forked at (`from` only).
96#[derive(Debug, Serialize, Deserialize)]
97#[serde(untagged)]
98pub enum Moment {
99    Fork(Fork),
100    At(Selection),
101}
102
103/// The moment `fork` forked at.
104#[derive(Debug, Serialize, Deserialize)]
105#[serde(deny_unknown_fields)]
106pub struct Fork {
107    pub fork: String,
108}
109
110/// The two moments, the page and the row form a caller asks for.
111#[derive(Debug, Serialize, Deserialize)]
112#[serde(deny_unknown_fields)]
113pub struct ChangesRequest {
114    pub from: Moment,
115    pub to: Moment,
116    /// `"origins"` (the default) or `"values"`.
117    #[serde(default = "origins")]
118    pub rows: Rows,
119    pub page: QueryPage,
120    #[serde(default)]
121    pub continuation: Option<String>,
122}
123
124const fn origins() -> Rows {
125    Rows::Origins
126}
127
128/// What one call of `changes.list` reads with, all owned by the service.
129pub struct Context<'a> {
130    pub access: &'a Access,
131    pub reader: &'a ReadOnlyDatabase,
132    pub catalogue: &'a Catalogue,
133    pub settings: &'a Settings,
134    pub seal: &'a Seal,
135    /// Who asked: the caller the service established for the request, whom
136    /// every continuation of it is bound to (ACCESS-003 R4).
137    pub author: &'a Caller,
138}
139
140/// One page of the changes `request` asks for.
141///
142/// # Errors
143///
144/// A named `Failure` for every refusal; never a shortened answer.
145pub fn list(
146    context: &Context<'_>,
147    request: &ChangesRequest,
148    response_byte_limit: Option<NonZeroUsize>,
149) -> Result<Box<RawValue>, Failure> {
150    let asked = checked(request, context.settings)?;
151    let token = request
152        .continuation
153        .as_deref()
154        .map(|encoded| Continuation::decode(encoded, context.seal))
155        .transpose()?;
156    if let Some(token) = &token {
157        token.check_moment(&asked.branch, asked.version)?;
158    }
159    // Both moments are held until this answer is built, on every path
160    // (STOR-003 H1b).
161    let mut from = match &request.from {
162        Moment::At(selection) => context.catalogue.capture_held(selection)?,
163        Moment::Fork(fork) => fork_moment(context, &fork.fork)?,
164    };
165    // The head is captured on the first page; a later page reads at the
166    // version its token carries.
167    let mut to = context.catalogue.capture_held(&Selection {
168        branch: asked.branch.clone(),
169        version: token.as_ref().map(|token| token.version).or(asked.version),
170    })?;
171    from.shards.sort_unstable_by_key(|shard| shard.shard_id);
172    to.shards.sort_unstable_by_key(|shard| shard.shard_id);
173    let binding = bind(request, from.version, context.settings, context.author)?;
174    if let Some(token) = &token {
175        token.check_binding(&binding)?;
176    }
177    let budget = budget(request.page.bytes, response_byte_limit)?;
178    let widest = widest_next(&to, &binding, context.seal)?;
179    let envelope = answer(&from, &to, &json!([]), Some(&widest), u64::MAX);
180    let allowance = Allowance {
181        rows: request.page.rows,
182        bytes: rows_bytes(budget, &envelope)?,
183        scan_records: context.settings.scan_records.get(),
184    };
185    let resume = token.as_ref().map(|token| &token.resume);
186    let after = resume
187        .map(|resume| revision_key(&resume.pending.collection, &resume.pending.id))
188        .map(|mut key| {
189            key.push(0); // Exclusive of the key given; ids never contain NUL.
190            key
191        });
192    let read_before = resume.map_or(0, |resume| resume.records_read);
193    let mut source = Changes::new(
194        context.reader,
195        &from,
196        &to,
197        after.as_deref(),
198        context.settings,
199    )?;
200    drop(after);
201    let reading = Reading {
202        context,
203        from: &from,
204        to: &to,
205        form: request.rows,
206    };
207    let page = page(&mut source, &reading, &allowance, read_before)?;
208    let next = page
209        .next
210        .map(|place| {
211            let resume = Resume {
212                records_read: page.given,
213                pending: place,
214                discard_outputs: 0,
215                passed: true,
216                passed_counts: Vec::new(),
217            };
218            let now = raw("null")?;
219            Continuation::new(to.branch.clone(), to.version, resume, now, binding)?
220                .encode(context.seal)
221        })
222        .transpose()?;
223    // NB1: the rows are written as text, each body as it is stored, and
224    // never decoded into a value and encoded again.
225    let rows = serde_json::value::to_raw_value(&page.rows).map_err(Failure::storage)?;
226    let envelope = answer(&from, &to, &json!([]), next.as_deref(), page.given);
227    let answer = with_rows(envelope, &rows)?;
228    if answer.get().len() > budget.get() {
229        return Err(Failure::new(
230            "response_limit",
231            "the answer exceeds its requested bytes",
232        ));
233    }
234    Ok(answer)
235}
236
237/// An answer object's members beside a `rows` member written as text (NB1).
238#[derive(Serialize)]
239struct WithRows<'a> {
240    #[serde(flatten)]
241    envelope: &'a serde_json::Map<String, Value>,
242    rows: &'a RawValue,
243}
244
245/// `envelope`, an object, as text with `rows` in place of its own `rows`
246/// member: every body inside `rows` goes out as the text it is.
247///
248/// # Errors
249/// `storage` when `envelope` is not an object or cannot be written.
250pub fn with_rows(envelope: Value, rows: &RawValue) -> Result<Box<RawValue>, Failure> {
251    let Value::Object(mut envelope) = envelope else {
252        return Err(Failure::new(
253            "storage",
254            "an answer envelope is not an object",
255        ));
256    };
257    envelope.remove("rows");
258    serde_json::value::to_raw_value(&WithRows {
259        envelope: &envelope,
260        rows,
261    })
262    .map_err(Failure::storage)
263}
264
265/// `to` as asked, once `to` is no fork, `from` names a version or a fork on
266/// `to`'s branch, and the page is within the service's.
267fn checked<'a>(request: &'a ChangesRequest, settings: &Settings) -> Result<&'a Selection, Failure> {
268    if request.page.rows.get() > u64::from(settings.max_page.get()) {
269        return Err(Failure::new(
270            "page_limit",
271            format!("maximum page size is {}", settings.max_page),
272        ));
273    }
274    let Moment::At(to) = &request.to else {
275        return Err(Failure::new(
276            "changes_fork_as_to",
277            "to names a branch and perhaps a version, never a fork",
278        ));
279    };
280    let branch = match &request.from {
281        Moment::At(from) if from.version.is_none() => {
282            return Err(Failure::new(
283                "changes_version_required",
284                "from names a version or a fork",
285            ));
286        }
287        Moment::At(from) => &from.branch,
288        Moment::Fork(fork) => &fork.fork,
289    };
290    if *branch != to.branch {
291        return Err(Failure::new(
292            "changes_branch_mismatch",
293            "from and to must be on the same branch",
294        ));
295    }
296    Ok(to)
297}
298
299/// Where a branch's fork provenance is kept (storage/branches.rs).
300const FORK_PROVENANCE: &[u8] = b"b\0fork";
301
302/// The moment `branch` forked at: its parent at the version it forked from.
303fn fork_moment(context: &Context<'_>, branch: &str) -> Result<Held, Failure> {
304    let head = context.catalogue.capture_held(&Selection {
305        branch: branch.to_owned(),
306        version: None,
307    })?;
308    let bytes = reads::get_bytes(context.reader, &head, FORK_PROVENANCE)?.ok_or_else(|| {
309        Failure::new(
310            "changes_fork_missing",
311            format!("branch {branch} has no recorded fork origin"),
312        )
313    })?;
314    let (fork, _line) = crate::serve::storage::branches::decode(&bytes)?;
315    context.catalogue.capture_held(&Selection {
316        branch: fork.branch,
317        version: Some(fork.version),
318    })
319}
320
321/// What a continuation is bound to: both moments as asked and the version
322/// `from` was found at, the caller, the page's limits and the row form.
323fn bind(
324    request: &ChangesRequest,
325    from: Version,
326    settings: &Settings,
327    author: &Caller,
328) -> Result<Binding, Failure> {
329    let asked = (&request.from, &request.to, from);
330    let moments = serde_json::to_string(&asked).map_err(Failure::storage)?;
331    let engine = Engine {
332        protocol: 1,
333        worker: "haem".to_owned(),
334        engine: "changes".to_owned(),
335        jaq_core: String::new(),
336        jaq_std: String::new(),
337        jaq_json: String::new(),
338    };
339    let limits = Allowance {
340        rows: request.page.rows,
341        bytes: request.page.bytes,
342        scan_records: settings.scan_records.get(),
343    };
344    Binding::new(
345        &moments,
346        &BTreeMap::new(),
347        &engine,
348        author,
349        &limits,
350        request.rows,
351    )
352}
353
354fn raw(json: &str) -> Result<Box<RawValue>, Failure> {
355    RawValue::from_string(json.to_owned()).map_err(Failure::storage)
356}
357
358/// The longest `next` a page could give: a place with the longest collection
359/// and id, every byte escaped, and the largest count.
360fn widest_next(to: &Captured, binding: &Binding, seal: &Seal) -> Result<String, Failure> {
361    let resume = Resume {
362        records_read: u64::MAX,
363        pending: Origin {
364            collection: "\u{1}".repeat(ID_BYTES),
365            id: "\u{1}".repeat(ID_BYTES),
366            version: raw("null")?,
367            score: None,
368        },
369        discard_outputs: u64::MAX,
370        passed: false,
371        passed_counts: Vec::new(),
372    };
373    let now = raw("null")?;
374    Continuation::new(to.branch.clone(), to.version, resume, now, binding.clone())?.encode(seal)
375}
376
377/// The smaller of the request's bytes and the transport's limit.
378fn budget(
379    requested: NonZeroU64,
380    response_byte_limit: Option<NonZeroUsize>,
381) -> Result<NonZeroUsize, Failure> {
382    let requested = NonZeroUsize::try_from(requested)
383        .map_err(|_| Failure::new("page_limit", "the requested bytes do not fit this machine"))?;
384    let bytes = response_byte_limit
385        .map_or_else(|| requested.get(), |limit| limit.get().min(requested.get()));
386    NonZeroUsize::new(bytes)
387        .ok_or_else(|| Failure::new("page_limit", "selected byte limit is zero"))
388}
389
390/// The bytes left for the rows once the answer's `envelope` is counted.
391fn rows_bytes(budget: NonZeroUsize, envelope: &Value) -> Result<NonZeroU64, Failure> {
392    let envelope = serde_json::to_vec(envelope)
393        .map_err(Failure::storage)?
394        .len();
395    // The envelope's empty `[]` is counted again by the page as its rows.
396    // It holds room for the widest continuation the page could need, so the
397    // refusal names the size: a caller asks once more, with enough.
398    let needed = envelope - 2;
399    u64::try_from(budget.get())
400        .ok()
401        .and_then(|budget| budget.checked_sub(u64::try_from(needed).ok()?))
402        .and_then(NonZeroU64::new)
403        .ok_or_else(|| {
404            Failure::new(
405                "response_limit",
406                format!(
407                    "the answer's envelope takes {needed} bytes before any row; ask for more than that"
408                ),
409            )
410        })
411}
412
413fn moment(at: &Captured) -> Value {
414    json!({"branch": at.branch, "version": at.version})
415}
416
417fn answer(from: &Captured, to: &Captured, rows: &Value, next: Option<&str>, read: u64) -> Value {
418    json!({
419        "from": moment(from),
420        "to": moment(to),
421        "rows": rows,
422        "next": next,
423        "changes_read": read,
424    })
425}
426
427/// The moments and row form one page is read with.
428struct Reading<'a> {
429    context: &'a Context<'a>,
430    from: &'a Captured,
431    to: &'a Captured,
432    form: Rows,
433}
434
435/// One page's rows, how many changes have been given in all, and where the
436/// next page starts after.
437struct Page {
438    rows: Vec<Row>,
439    given: u64,
440    next: Option<Origin>,
441}
442
443/// The rows of one page: changes the caller may read, until the rows, the
444/// bytes or the source's limit are reached.
445fn page(
446    source: &mut Changes<'_>,
447    reading: &Reading<'_>,
448    allowance: &Allowance,
449    read_before: u64,
450) -> Result<Page, Failure> {
451    let mut page = Page {
452        rows: Vec::new(),
453        given: read_before,
454        next: None,
455    };
456    // The rows array's brackets.
457    let mut bytes = 2_u64;
458    // The last key this page finished with, given or passed.
459    let mut done: Option<Vec<u8>> = None;
460    loop {
461        let given = u64::try_from(page.rows.len()).map_err(Failure::storage)?;
462        if given == allowance.rows.get() {
463            page.next = done.as_deref().map(place).transpose()?;
464            return Ok(page);
465        }
466        let change = match source.next()? {
467            Next::Change(change) => change,
468            Next::Stopped(key) => {
469                page.next = Some(place(&key)?);
470                return Ok(page);
471            }
472            Next::End => return Ok(page),
473        };
474        let key = changed_key(&change).to_vec();
475        let Some(row) = row(reading, change)? else {
476            done = Some(key);
477            continue;
478        };
479        let size = u64::try_from(serde_json::to_vec(&row).map_err(Failure::storage)?.len())
480            .map_err(Failure::storage)?;
481        if size.saturating_add(2) > allowance.bytes.get() {
482            return Err(Failure::new(
483                "changes_row_too_large",
484                format!("a change needs more than {} bytes", allowance.bytes),
485            ));
486        }
487        let grown = bytes
488            .saturating_add(u64::from(given > 0))
489            .saturating_add(size);
490        if grown > allowance.bytes.get() {
491            // Not given: the next page reads it again.
492            page.next = done.as_deref().map(place).transpose()?;
493            return Ok(page);
494        }
495        bytes = grown;
496        page.given = page.given.saturating_add(1);
497        page.rows.push(row);
498        done = Some(key);
499    }
500}
501
502fn changed_key(change: &ObservedChange) -> &[u8] {
503    match change {
504        ObservedChange::Added { key, .. }
505        | ObservedChange::Removed { key, .. }
506        | ObservedChange::Modified { key, .. } => key,
507    }
508}
509
510/// A revision key's collection and id.
511fn parts(key: &[u8]) -> Result<(&str, &str), Failure> {
512    key.strip_prefix(REVISIONS)
513        .and_then(|rest| std::str::from_utf8(rest).ok())
514        .and_then(|rest| rest.split_once('\0'))
515        .ok_or_else(|| Failure::new("changes_key_corrupt", "a revision key names no record"))
516}
517
518/// The place after which the next page starts.
519fn place(key: &[u8]) -> Result<Origin, Failure> {
520    let (collection, id) = parts(key)?;
521    Ok(Origin {
522        collection: collection.to_owned(),
523        id: id.to_owned(),
524        version: raw("null")?,
525        score: None,
526    })
527}
528
529fn written_at(revision: &[u8]) -> Result<Version, Failure> {
530    serde_json::from_slice::<DocumentRevision>(revision)
531        .map(|revision| revision.written_at)
532        .map_err(|error| Failure::new("changes_revision_corrupt", error.to_string()))
533}
534
535/// A changed record's name.
536#[derive(Serialize)]
537struct Named {
538    collection: String,
539    id: String,
540}
541
542/// One change. Without values, `value_before` and `value_after` are absent;
543/// with them, each is the body at its moment, or null where there is none.
544#[derive(Serialize)]
545struct Row {
546    kind: &'static str,
547    origin: Named,
548    before: Option<Version>,
549    after: Option<Version>,
550    #[serde(skip_serializing_if = "Option::is_none")]
551    value_before: Option<Box<RawValue>>,
552    #[serde(skip_serializing_if = "Option::is_none")]
553    value_after: Option<Box<RawValue>>,
554}
555
556/// The row for `change`, or none when the caller may not read its record.
557fn row(reading: &Reading<'_>, change: ObservedChange) -> Result<Option<Row>, Failure> {
558    let (key, kind, before, after) = match change {
559        ObservedChange::Added { key, value } => (key, "added", None, Some(value)),
560        ObservedChange::Removed { key, value } => (key, "removed", Some(value), None),
561        ObservedChange::Modified {
562            key,
563            old_value,
564            new_value,
565        } => (key, "changed", Some(old_value), Some(new_value)),
566    };
567    let (collection, id) = parts(&key)?;
568    if !reading.context.access.can_read_document(collection, id) {
569        return Ok(None);
570    }
571    let before = before.as_deref().map(written_at).transpose()?;
572    let after = after.as_deref().map(written_at).transpose()?;
573    let mut row = Row {
574        kind,
575        origin: Named {
576            collection: collection.to_owned(),
577            id: id.to_owned(),
578        },
579        before,
580        after,
581        value_before: None,
582        value_after: None,
583    };
584    if reading.form == Rows::Values {
585        let body = document_key(collection, id);
586        row.value_before = Some(body_at(reading, reading.from, &body, before.is_some())?);
587        row.value_after = Some(body_at(reading, reading.to, &body, after.is_some())?);
588    }
589    Ok(Some(row))
590}
591
592/// The body at `at` of a record whose revision is there (`present`), else
593/// null: the stored text as it is (NB1), checked to be JSON and never
594/// decoded into a value, so every number keeps the text it was written with.
595fn body_at(
596    reading: &Reading<'_>,
597    at: &Captured,
598    key: &[u8],
599    present: bool,
600) -> Result<Box<RawValue>, Failure> {
601    if !present {
602        return raw("null");
603    }
604    #[cfg(test)]
605    BODIES.with(|count| count.set(count.get() + 1));
606    let bytes = reads::get_bytes(reading.context.reader, at, key)?
607        .ok_or_else(|| Failure::new("changes_body_missing", "a revision has no body"))?;
608    let text = std::str::from_utf8(&bytes).map_err(Failure::storage)?;
609    RawValue::from_string(text.to_owned()).map_err(Failure::storage)
610}
611
612/// What the source gives next.
613enum Next {
614    Change(ObservedChange),
615    /// The limit is reached with changes left: the last key taken.
616    Stopped(Vec<u8>),
617    End,
618}
619
620/// One shard's diff, read ahead after its last key.
621struct Shard {
622    id: usize,
623    from: Hash,
624    to: Hash,
625    after: Option<Vec<u8>>,
626    ahead: VecDeque<ObservedChange>,
627    ended: bool,
628}
629
630/// The revision changes of every shard between two captures, in key order.
631struct Changes<'a> {
632    reader: &'a ReadOnlyDatabase,
633    shards: Vec<Shard>,
634    node_budget: NonZeroUsize,
635    limit: u64,
636    taken: u64,
637    last: Option<Vec<u8>>,
638}
639
640impl<'a> Changes<'a> {
641    fn new(
642        reader: &'a ReadOnlyDatabase,
643        from: &Captured,
644        to: &Captured,
645        after: Option<&[u8]>,
646        settings: &Settings,
647    ) -> Result<Self, Failure> {
648        if from.shards.len() != to.shards.len() {
649            return Err(Failure::new(
650                "changes_shards_differ",
651                "the two moments do not have the same shards",
652            ));
653        }
654        let mut shards = Vec::with_capacity(to.shards.len());
655        for (before, now) in from.shards.iter().zip(&to.shards) {
656            if before.shard_id != now.shard_id {
657                return Err(Failure::new(
658                    "changes_shards_differ",
659                    "the two moments do not name the same shards",
660                ));
661            }
662            shards.push(Shard {
663                id: now.shard_id,
664                from: before.head,
665                to: now.head,
666                after: after.map(<[u8]>::to_vec),
667                ahead: VecDeque::new(),
668                // Equal roots have no changes: nothing is read.
669                ended: before.head == now.head,
670            });
671        }
672        Ok(Self {
673            reader,
674            shards,
675            node_budget: settings.node_budget,
676            limit: settings.scan_records.get(),
677            taken: 0,
678            last: None,
679        })
680    }
681
682    /// As many changes as this page may still take, at most `BATCH`, at least one.
683    fn batch(&self) -> NonZeroUsize {
684        let left = usize::try_from(self.limit.saturating_sub(self.taken)).unwrap_or(usize::MAX);
685        NonZeroUsize::new(left.min(BATCH)).unwrap_or(NonZeroUsize::MIN)
686    }
687
688    /// Reads on every shard whose read-ahead is empty until it has a change or
689    /// its diff is done; a diff page may be empty with more to come.
690    fn fill(&mut self) -> Result<(), Failure> {
691        let budget = DiffBudget {
692            max_changes: self.batch(),
693            max_nodes: self.node_budget,
694        };
695        let interval = DiffInterval {
696            lower: Some(REVISIONS),
697            upper: Some(REVISIONS_END),
698        };
699        for shard in &mut self.shards {
700            while shard.ahead.is_empty() && !shard.ended {
701                #[cfg(test)]
702                DIFFS.with(|count| count.set(count.get() + 1));
703                let page = self
704                    .reader
705                    .changes_page_in_range(
706                        shard.id,
707                        shard.from,
708                        shard.to,
709                        interval,
710                        shard.after.as_deref(),
711                        budget,
712                    )
713                    .map_err(diff_failure)?;
714                shard.ended = page.next_after.is_none();
715                shard.after = page.next_after;
716                shard.ahead.extend(page.entries);
717            }
718        }
719        Ok(())
720    }
721
722    fn next(&mut self) -> Result<Next, Failure> {
723        if self.taken >= self.limit {
724            if self.shards.iter().all(|shard| shard.ahead.is_empty()) {
725                self.fill()?;
726            }
727            if self.shards.iter().all(|shard| shard.ahead.is_empty()) {
728                return Ok(Next::End);
729            }
730            let last = self.last.clone().ok_or_else(|| {
731                Failure::new(
732                    "changes_scan_limit",
733                    "a page that may take no changes moves nowhere",
734                )
735            })?;
736            return Ok(Next::Stopped(last));
737        }
738        self.fill()?;
739        let Some(shard) = self
740            .shards
741            .iter_mut()
742            .filter(|shard| !shard.ahead.is_empty())
743            .min_by(|left, right| {
744                let left = left.ahead.front().map(changed_key);
745                let right = right.ahead.front().map(changed_key);
746                left.cmp(&right)
747            })
748        else {
749            return Ok(Next::End);
750        };
751        let Some(change) = shard.ahead.pop_front() else {
752            return Ok(Next::End);
753        };
754        self.taken += 1;
755        self.last = Some(changed_key(&change).to_vec());
756        Ok(Next::Change(change))
757    }
758}
759
760/// A moment whose nodes are gone is named, never read around.
761fn diff_failure(error: ObserverError) -> Failure {
762    let classification = match &error {
763        ObserverError::Tree(TreeError::MissingNode { hash }) => Failure::new(
764            "changes_moment_gone",
765            format!("the moment no longer holds node {hash}"),
766        ),
767        ObserverError::RootUnavailable { shard_id, root } => Failure::new(
768            "changes_moment_gone",
769            format!("shard {shard_id} cannot read root {root}"),
770        ),
771        ObserverError::Diff(error @ DiffError::NodeBudgetTooSmall { .. }) => {
772            Failure::new("changes_node_budget", error.to_string())
773        }
774        other => Failure::storage(other),
775    };
776    let mut failure = Failure::observer(error);
777    failure.code = classification.code;
778    failure.message = classification.message;
779    failure.path = classification.path;
780    failure
781}
782
783#[cfg(test)]
784#[path = "changes_tests.rs"]
785mod tests;
786
787#[cfg(test)]
788#[path = "held_reads_tests.rs"]
789mod held_reads_tests;