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 pub author: &'a Caller,
139}
140
141pub 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 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 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); 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 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#[derive(Serialize)]
240struct WithRows<'a> {
241 #[serde(flatten)]
242 envelope: &'a serde_json::Map<String, Value>,
243 rows: &'a RawValue,
244}
245
246pub 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
266fn 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
300const FORK_PROVENANCE: &[u8] = b"b\0fork";
302
303fn 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
322fn 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
359fn 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
378fn 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
391fn 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 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
428struct Reading<'a> {
430 context: &'a Context<'a>,
431 from: &'a Captured,
432 to: &'a Captured,
433 form: Rows,
434}
435
436struct Page {
439 rows: Vec<Row>,
440 given: u64,
441 next: Option<Origin>,
442}
443
444fn 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 let mut bytes = 2_u64;
459 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 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
511fn 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
519fn 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#[derive(Serialize)]
538struct Named {
539 collection: String,
540 id: String,
541}
542
543#[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
557fn 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
593fn 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
613enum Next {
615 Change(ObservedChange),
616 Stopped(Vec<u8>),
618 End,
619}
620
621struct Shard {
623 id: usize,
624 from: Hash,
625 to: Hash,
626 after: Option<Vec<u8>>,
627 ahead: VecDeque<ObservedChange>,
628 ended: bool,
629}
630
631struct 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 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 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 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
761fn 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;