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