1use crate::row_scope::{RowImage, RowScope};
5use crate::scope_lookup::Lookups;
6use crate::sql_value::SqlValue;
7use crate::{CommitScope, DbError};
8use coven_foundation::locks::lock_unpoisoned;
9use coven_foundation::task::{Bug, BugSinkRef};
10use std::{
11 cell::RefCell,
12 collections::{BTreeMap, BTreeSet, HashMap},
13 sync::{Arc, Mutex, Weak},
14};
15use tokio::sync::watch;
16
17pub(crate) type ColumnSet = BTreeSet<(String, String)>;
18pub(crate) type ReadSet = Vec<TableRead>;
19
20const UNOBSERVED: [&str; 23] = [
25 "_coven_authored_admissions",
26 "_coven_authored_changes",
27 "_coven_authored_parts",
28 "_coven_cells",
29 "_coven_claims",
30 "_coven_creations",
31 "_coven_foreign_keys",
32 "_coven_part_leaves",
33 "_coven_part_proofs",
34 "_coven_published",
35 "_coven_published_creations",
36 "_coven_published_dropped",
37 "_coven_published_file_refs",
38 "_coven_published_losses",
39 "_coven_published_rows",
40 "_coven_published_writes",
41 "_coven_references",
42 "_coven_rows",
43 "_coven_unnamed_writes",
44 "_coven_write_evidence",
45 "_coven_write_headers",
46 "_coven_constraints",
47 "_coven_written_values",
48];
49
50pub(crate) fn observed(table: &str) -> bool {
52 !UNOBSERVED.iter().any(|name| name.eq_ignore_ascii_case(table))
53}
54
55#[derive(Clone)]
56pub(crate) struct TableRead {
57 pub(crate) table: String,
58 pub(crate) columns: BTreeSet<String>,
59 pub(crate) scope: RowScope,
61 pub(crate) inserted_only: bool,
63}
64
65pub(crate) struct RowChange {
66 pub(crate) table: String,
67 pub(crate) column: String,
68 pub(crate) rows: Option<(Option<RowImage>, Option<RowImage>)>,
72}
73
74impl TableRead {
75 fn affected(&self, change: &RowChange, lookups: &mut Lookups<'_>) -> rusqlite::Result<bool> {
78 if self.table != change.table
79 || !(change.column.is_empty() || self.columns.contains(&change.column))
80 || (self.inserted_only && !inserted(change))
81 {
82 return Ok(false);
83 }
84 let Some((old, new)) = &change.rows else {
85 return Ok(true);
86 };
87 for row in [old, new].into_iter().flatten() {
88 if self.scope.matches(row, lookups)? {
89 return Ok(true);
90 }
91 }
92 Ok(false)
93 }
94}
95
96fn inserted(change: &RowChange) -> bool {
98 change.column.is_empty() && change.rows.as_ref().is_some_and(|(old, _)| old.is_none())
99}
100
101#[derive(Clone)]
103pub(crate) enum HandleFailure {
104 State(Arc<DbError>),
106 Rollback,
109 Checkpoint(Arc<DbError>),
112 Statistics(Arc<DbError>),
116 WallTimer(coven_foundation::clock::WallTimerError),
118 Bug(Bug),
122}
123
124impl HandleFailure {
125 fn error(&self) -> DbError {
126 match self {
127 Self::State(cause) => DbError::StatePersistence(cause.clone()),
128 Self::Rollback => DbError::RollbackFailed,
129 Self::Checkpoint(cause) => DbError::CheckpointFailed(cause.clone()),
130 Self::Statistics(cause) => DbError::StatisticsFailed(cause.clone()),
131 Self::WallTimer(cause) => DbError::WallTimer(cause.clone()),
132 Self::Bug(bug) => DbError::StoreFailed(bug.clone()),
133 }
134 }
135}
136
137#[derive(Clone)]
138pub(crate) struct CommitState {
139 pub(crate) changed: bool,
140 closed: bool,
141 failure: Option<HandleFailure>,
142}
143
144impl CommitState {
145 pub(crate) fn closed(&self) -> bool {
147 self.closed
148 }
149
150 pub(crate) fn error(&self) -> Option<DbError> {
151 self.failure
152 .as_ref()
153 .map(HandleFailure::error)
154 .or_else(|| self.closed.then_some(DbError::StoreClosed))
155 }
156}
157
158enum Tracking {
159 Reading {
163 commits: Vec<Arc<Vec<RowChange>>>,
164 previous: Arc<ReadSet>,
165 },
166 Waiting(Arc<ReadSet>),
167}
168
169impl Tracking {
170 fn scoped(&self) -> &Arc<ReadSet> {
174 match self {
175 Self::Reading { previous: reads, .. } | Self::Waiting(reads) => reads,
176 }
177 }
178}
179
180struct Tracked {
183 tracking: Tracking,
184 epoch: u64,
185}
186
187struct SubscriptionState {
188 tracking: Mutex<Tracked>,
189 state: watch::Sender<CommitState>,
190 outside: Option<CommitScope>,
192}
193
194#[derive(Clone)]
195pub(crate) struct CommitSubscription {
196 shared: Arc<SubscriptionState>,
197 receiver: watch::Receiver<CommitState>,
198}
199
200impl CommitSubscription {
201 fn new(closed: bool, failure: Option<HandleFailure>, outside: Option<CommitScope>) -> Self {
202 let (state, receiver) = watch::channel(CommitState { changed: false, closed, failure });
203 Self {
204 shared: Arc::new(SubscriptionState {
205 tracking: Mutex::new(Tracked {
206 tracking: Tracking::Waiting(Arc::new(Vec::new())),
207 epoch: 0,
208 }),
209 state,
210 outside,
211 }),
212 receiver,
213 }
214 }
215
216 pub(crate) fn state(&self) -> CommitState {
217 self.receiver.borrow().clone()
218 }
219
220 pub(crate) async fn changed(&mut self) {
221 self.receiver.changed().await.expect("subscription owns its sender");
222 }
223
224 pub(crate) fn begin(&self) {
228 let mut tracked = lock_unpoisoned(&self.shared.tracking);
229 let previous = Arc::clone(tracked.tracking.scoped());
230 tracked.tracking = Tracking::Reading { commits: Vec::new(), previous };
231 tracked.epoch += 1;
232 self.shared.state.send_modify(|state| state.changed = false);
233 }
234
235 pub(crate) fn stop(&self) {
238 let mut tracked = lock_unpoisoned(&self.shared.tracking);
239 tracked.tracking = Tracking::Waiting(Arc::new(Vec::new()));
240 tracked.epoch += 1;
241 }
242
243 pub(crate) fn finish(&self, reads: ReadSet) {
245 self.finish_judging(reads, &mut |_, _| {
246 unreachable!("only a live query's run reads a join")
247 })
248 .expect("judging without lookups can't fail");
249 }
250
251 pub(crate) fn finish_judging(
257 &self,
258 reads: ReadSet,
259 lookups: &mut Lookups<'_>,
260 ) -> rusqlite::Result<()> {
261 if let Some(read) = reads.iter().find(|read| !observed(&read.table)) {
262 panic!("a live query read {}, whose changes no write records", read.table);
263 }
264 let reads = Arc::new(reads);
265 let mut changed = false;
266 let mut failure = None;
267 let mut answers = Answers::default();
268 loop {
269 let commits = {
272 let mut tracked = lock_unpoisoned(&self.shared.tracking);
273 let Tracking::Reading { commits, .. } = &mut tracked.tracking else {
274 panic!("read observation was not started")
275 };
276 if commits.is_empty() {
277 tracked.tracking = Tracking::Waiting(Arc::clone(&reads));
278 tracked.epoch += 1;
279 self.shared.state.send_modify(|state| state.changed = changed);
280 break;
281 }
282 std::mem::take(commits)
283 };
284 for changes in commits {
285 if changed {
286 break;
287 }
288 match affected(&reads, &changes, &mut answers, lookups) {
289 Ok(found) => changed = found,
290 Err(error) => {
291 changed = true;
292 failure = Some(error);
293 }
294 }
295 }
296 }
297 failure.map_or(Ok(()), Err)
298 }
299}
300
301fn affected(
306 reads: &ReadSet,
307 changes: &[RowChange],
308 answers: &mut Answers,
309 lookups: &mut Lookups<'_>,
310) -> rusqlite::Result<bool> {
311 let mut counts: HashMap<&str, usize> = HashMap::new();
312 for change in changes.iter().filter(|change| change.rows.is_some()) {
313 *counts.entry(&change.table).or_default() += 1;
314 }
315 for change in changes {
316 let bounded = counts
317 .get(change.table.as_str())
318 .is_some_and(|count| *count > crate::sqlite::ROWS_PER_TABLE);
319 for read in reads {
320 let found = if bounded {
321 read.affected(change, &mut |_, _| Ok(true))?
322 } else {
323 read.affected(change, &mut |lookup, value| answers.answer(lookup, value, lookups))?
324 };
325 if found {
326 return Ok(true);
327 }
328 }
329 }
330 Ok(false)
331}
332
333#[derive(Default)]
336struct Answers(HashMap<(usize, ValueKey), bool>);
337
338impl Answers {
339 fn answer(
340 &mut self,
341 lookup: &crate::scope_lookup::Lookup,
342 value: &SqlValue,
343 lookups: &mut Lookups<'_>,
344 ) -> rusqlite::Result<bool> {
345 let key = (std::ptr::from_ref(lookup) as usize, ValueKey::of(value));
346 if let Some(found) = self.0.get(&key) {
347 return Ok(*found);
348 }
349 let found = lookups(lookup, value)?;
350 self.0.insert(key, found);
351 Ok(found)
352 }
353}
354
355#[derive(Hash, PartialEq, Eq)]
357enum ValueKey {
358 Null,
359 Integer(i64),
360 Real(u64),
361 Text(Vec<u8>),
362 Blob(Vec<u8>),
363}
364
365impl ValueKey {
366 fn of(value: &SqlValue) -> Self {
367 match value {
368 SqlValue::Null => Self::Null,
369 SqlValue::Integer(value) => Self::Integer(*value),
370 SqlValue::Real(value) => Self::Real(value.to_bits()),
371 SqlValue::Text(value) => Self::Text(value.clone()),
372 SqlValue::Blob(value) => Self::Blob(value.clone()),
373 }
374 }
375}
376
377pub(crate) struct Judged {
380 changes: Arc<Vec<RowChange>>,
381 verdicts: Vec<(Weak<SubscriptionState>, u64, bool)>,
384}
385
386impl Judged {
387 pub(crate) fn unjudged(changes: Vec<RowChange>) -> Self {
390 Self { changes: Arc::new(changes), verdicts: Vec::new() }
391 }
392}
393
394pub struct AttemptCommits {
397 commits: CommitSubscription,
398}
399
400struct ObserverState {
401 subscriptions: Vec<Weak<SubscriptionState>>,
402 closed: bool,
403 failure: Option<HandleFailure>,
404}
405
406thread_local! {
407 #[cfg_attr(target_os = "android", allow(clippy::missing_const_for_thread_local))]
413 static CALL: RefCell<Option<Call>> = const { RefCell::new(None) };
414}
415
416struct Call {
418 handle: usize,
420 bug: Option<Bug>,
423}
424
425#[track_caller]
428fn half_changed(what: &'static str) -> Bug {
429 Bug::new(what, "a panic while it was locked may have left it half-changed")
430}
431
432impl ObserverState {
433 fn fail(&mut self, failure: HandleFailure) {
435 let error = self.failure.get_or_insert(failure).clone();
436 self.subscriptions.retain(|subscription| {
437 let Some(subscription) = subscription.upgrade() else {
438 return false;
439 };
440 subscription.state.send_modify(|state| state.failure = Some(error.clone()));
441 true
442 });
443 }
444
445 fn close(&mut self) {
446 self.closed = true;
447 self.subscriptions.retain(|subscription| {
448 let Some(subscription) = subscription.upgrade() else {
449 return false;
450 };
451 subscription.state.send_modify(|state| state.closed = true);
452 true
453 });
454 }
455}
456
457impl Drop for ObserverState {
458 fn drop(&mut self) {
459 self.close();
460 }
461}
462
463#[derive(Clone)]
464pub(crate) struct CommitObserver {
465 state: Arc<Mutex<ObserverState>>,
466 bugs: BugSinkRef,
468 #[cfg(test)]
471 upkeep_threads: Arc<Mutex<Option<usize>>>,
472}
473
474impl CommitObserver {
475 pub(crate) fn new(bugs: BugSinkRef) -> Self {
476 Self {
477 state: Arc::new(Mutex::new(ObserverState {
478 subscriptions: Vec::new(),
479 closed: false,
480 failure: None,
481 })),
482 bugs,
483 #[cfg(test)]
484 upkeep_threads: Arc::new(Mutex::new(None)),
485 }
486 }
487
488 #[cfg(test)]
490 pub(crate) fn refuse_upkeep_thread_after(&self, started: Option<usize>) {
491 *lock_unpoisoned(&self.upkeep_threads) = started;
492 }
493
494 #[cfg(test)]
496 pub(crate) fn upkeep_thread_refused(&self) -> bool {
497 let mut left = lock_unpoisoned(&self.upkeep_threads);
498 match left.as_mut() {
499 Some(0) => true,
500 Some(left) => {
501 *left -= 1;
502 false
503 }
504 None => false,
505 }
506 }
507
508 pub(crate) fn subscribe(&self) -> CommitSubscription {
509 self.subscribe_outside(None)
510 }
511
512 pub(crate) fn subscribe_outside(&self, outside: Option<CommitScope>) -> CommitSubscription {
516 self.with_state(|state| {
517 let failure = if state.closed { None } else { state.failure.clone() };
518 let subscription = CommitSubscription::new(state.closed, failure, outside);
519 state.subscriptions.retain(|subscription| subscription.strong_count() != 0);
520 state.subscriptions.push(Arc::downgrade(&subscription.shared));
521 subscription
522 })
523 }
524
525 pub(crate) fn changes(&self, reads: ReadSet, first: bool) -> crate::DatabaseChanges {
528 let commits = self.subscribe();
529 commits.begin();
530 commits.finish(reads.clone());
531 crate::DatabaseChanges::new(commits, reads, first)
532 }
533
534 pub(crate) fn attempt(&self, attempt: CommitScope) -> AttemptCommits {
537 let commits = self.subscribe_outside(Some(attempt));
538 commits.begin();
539 AttemptCommits { commits }
540 }
541
542 pub(crate) fn watch_attempt(
545 &self,
546 attempt: AttemptCommits,
547 rows: &[crate::PrerequisiteRows],
548 ) -> crate::DatabaseChanges {
549 let reads: ReadSet = rows.iter().flat_map(crate::PrerequisiteRows::reads).collect();
550 attempt.commits.finish(reads.clone());
551 crate::DatabaseChanges::new(attempt.commits, reads, false)
552 }
553
554 pub(crate) fn judge(
558 &self,
559 changes: Vec<RowChange>,
560 scope: Option<CommitScope>,
561 lookups: &mut Lookups<'_>,
562 ) -> rusqlite::Result<Judged> {
563 let waiting: Vec<(Arc<SubscriptionState>, u64, Arc<ReadSet>)> = self.with_state(|state| {
564 state
565 .subscriptions
566 .iter()
567 .filter_map(Weak::upgrade)
568 .filter(|subscription| scope.is_none() || scope != subscription.outside)
569 .filter(|subscription| !subscription.state.borrow().changed)
570 .filter_map(|subscription| {
571 let tracked = lock_unpoisoned(&subscription.tracking);
572 let Tracking::Waiting(reads) = &tracked.tracking else {
573 return None;
574 };
575 let found = (Arc::clone(reads), tracked.epoch);
576 drop(tracked);
577 Some((subscription, found.1, found.0))
578 })
579 .collect()
580 });
581 let mut answers = Answers::default();
582 let mut verdicts = Vec::with_capacity(waiting.len());
583 for (subscription, epoch, reads) in waiting {
584 let found = affected(&reads, &changes, &mut answers, lookups)?;
585 verdicts.push((Arc::downgrade(&subscription), epoch, found));
586 }
587 Ok(Judged { changes: Arc::new(changes), verdicts })
588 }
589
590 pub(crate) fn commit(&self, judged: Judged, scope: Option<CommitScope>) {
597 let Judged { changes, verdicts } = judged;
598 self.with_state(|state| {
599 state.subscriptions.retain(|subscription| {
600 let Some(subscription) = subscription.upgrade() else {
601 return false;
602 };
603 if scope.is_some() && scope == subscription.outside {
604 return true;
605 }
606 let mut tracked = lock_unpoisoned(&subscription.tracking);
607 let epoch = tracked.epoch;
608 match &mut tracked.tracking {
609 Tracking::Reading { commits, .. } => commits.push(Arc::clone(&changes)),
610 Tracking::Waiting(reads) => {
611 let verdict = verdicts.iter().find_map(|(judged, at, found)| {
612 (*at == epoch
613 && std::ptr::eq(judged.as_ptr(), Arc::as_ptr(&subscription)))
614 .then_some(*found)
615 });
616 let found = verdict.unwrap_or_else(|| {
617 affected(reads, &changes, &mut Answers::default(), &mut |_, _| Ok(true))
618 .expect("judging without lookups can't fail")
619 });
620 if found {
621 subscription.state.send_modify(|state| state.changed = true);
622 }
623 }
624 }
625 true
626 })
627 });
628 }
629
630 pub(crate) fn scoped_columns(&self) -> BTreeMap<String, BTreeSet<String>> {
636 self.with_state(|state| {
637 let mut tables: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
638 for subscription in state.subscriptions.iter().filter_map(Weak::upgrade) {
639 let reads = Arc::clone(lock_unpoisoned(&subscription.tracking).tracking.scoped());
640 for read in reads.iter().filter(|read| !matches!(read.scope, RowScope::All)) {
641 read.scope.columns(tables.entry(read.table.to_ascii_lowercase()).or_default());
642 }
643 }
644 tables
645 })
646 }
647
648 pub(crate) fn error(&self) -> Option<DbError> {
651 self.with_state(|state| {
652 state
653 .failure
654 .as_ref()
655 .map(HandleFailure::error)
656 .or_else(|| state.closed.then_some(DbError::StoreClosed))
657 })
658 }
659
660 pub(crate) fn fail(&self, failure: HandleFailure) {
662 self.with_state(|state| state.fail(failure));
663 }
664
665 pub(crate) fn fail_with_bug(&self, bug: Bug) {
672 {
673 let mut state = self.state.lock().unwrap_or_else(|poisoned| {
674 self.state.clear_poison();
675 poisoned.into_inner()
676 });
677 state.fail(HandleFailure::Bug(bug.clone()));
678 }
679 let handle = self.id();
680 let unclaimed = CALL
682 .try_with(|call| match call.borrow_mut().as_mut() {
683 Some(call) if call.handle == handle && call.bug.is_none() => {
684 call.bug = Some(bug.clone());
685 None
686 }
687 _ => Some(bug.clone()),
688 })
689 .unwrap_or(Some(bug));
690 if let Some(bug) = unclaimed {
691 self.report(&bug);
692 }
693 }
694
695 pub(crate) fn calling<R>(&self, call: impl FnOnce() -> R) -> (R, Option<Bug>) {
702 let handle = self.id();
703 if CALL
704 .with(|running| running.borrow().as_ref().is_some_and(|outer| outer.handle == handle))
705 {
706 return (call(), None);
707 }
708 struct CallScope<'a> {
710 outer: Option<Call>,
711 observer: &'a CommitObserver,
712 }
713 impl Drop for CallScope<'_> {
714 fn drop(&mut self) {
715 let ended = CALL.with(|call| call.replace(self.outer.take()));
716 if let Some(bug) = ended.and_then(|ended| ended.bug) {
717 self.observer.report(&bug);
718 }
719 }
720 }
721 let outer = CALL.with(|running| running.replace(Some(Call { handle, bug: None })));
722 let scope = CallScope { outer, observer: self };
723 let result = call();
724 let bug =
725 CALL.with(|running| running.borrow_mut().as_mut().and_then(|call| call.bug.take()));
726 drop(scope);
727 (result, bug)
728 }
729
730 pub(crate) fn report(&self, bug: &Bug) {
732 coven_foundation::task::report(self.bugs.as_ref(), bug);
733 }
734
735 fn id(&self) -> usize {
737 Arc::as_ptr(&self.state) as usize
738 }
739
740 pub(crate) fn close(&self) {
741 self.with_state(ObserverState::close);
742 }
743
744 #[cfg(test)]
747 pub(crate) fn test_poison(&self) {
748 std::thread::scope(|scope| {
749 let holder = scope.spawn(|| {
750 let _held = self.state.lock();
751 panic!("a live query's judgment broke");
752 });
753 assert!(holder.join().is_err());
754 });
755 }
756
757 fn with_state<R>(&self, change: impl FnOnce(&mut ObserverState) -> R) -> R {
762 let mut state = self.state.lock().unwrap_or_else(|poisoned| {
763 let mut state = poisoned.into_inner();
764 state.fail(HandleFailure::Bug(half_changed("the store's live queries")));
765 self.state.clear_poison();
766 state
767 });
768 change(&mut state)
769 }
770}
771
772#[cfg(test)]
773#[path = "observation_tests.rs"]
774mod tests;