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, 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 static DIFFS: Cell<u64> = const { Cell::new(0) };
63 static BODIES: Cell<u64> = const { Cell::new(0) };
64}
65
66const BATCH: usize = 64;
68
69const ID_BYTES: usize = 512;
71
72#[derive(Clone, Copy, Debug)]
74pub struct Settings {
75 pub scan_records: NonZeroU64,
77 pub node_budget: NonZeroUsize,
79 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 node_budget: NonZeroUsize::MIN.saturating_add(65_535),
89 max_page: NonZeroU32::MIN.saturating_add(999),
90 }
91 }
92}
93
94#[derive(Debug, Serialize, Deserialize)]
97#[serde(untagged)]
98pub enum Moment {
99 Fork(Fork),
100 At(Selection),
101}
102
103#[derive(Debug, Serialize, Deserialize)]
105#[serde(deny_unknown_fields)]
106pub struct Fork {
107 pub fork: String,
108}
109
110#[derive(Debug, Serialize, Deserialize)]
112#[serde(deny_unknown_fields)]
113pub struct ChangesRequest {
114 pub from: Moment,
115 pub to: Moment,
116 #[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
128pub 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 pub author: &'a Caller,
138}
139
140pub 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 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 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); 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 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#[derive(Serialize)]
239struct WithRows<'a> {
240 #[serde(flatten)]
241 envelope: &'a serde_json::Map<String, Value>,
242 rows: &'a RawValue,
243}
244
245pub 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
265fn 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
299const FORK_PROVENANCE: &[u8] = b"b\0fork";
301
302fn 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
321fn 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
358fn 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
377fn 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
390fn 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 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
427struct Reading<'a> {
429 context: &'a Context<'a>,
430 from: &'a Captured,
431 to: &'a Captured,
432 form: Rows,
433}
434
435struct Page {
438 rows: Vec<Row>,
439 given: u64,
440 next: Option<Origin>,
441}
442
443fn 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 let mut bytes = 2_u64;
458 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 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
510fn 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
518fn 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#[derive(Serialize)]
537struct Named {
538 collection: String,
539 id: String,
540}
541
542#[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
556fn 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
592fn 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
612enum Next {
614 Change(ObservedChange),
615 Stopped(Vec<u8>),
617 End,
618}
619
620struct Shard {
622 id: usize,
623 from: Hash,
624 to: Hash,
625 after: Option<Vec<u8>>,
626 ahead: VecDeque<ObservedChange>,
627 ended: bool,
628}
629
630struct 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 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 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 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
760fn 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;