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