Skip to main content

coven_database/
observation.rs

1//! Per-subscription dependencies. Commits during a read are retained until its
2//! dependencies are known; idle subscriptions retain only a changed bit.
3
4use 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
20/// Coven's tables no live query reads: the merge state, authorship, creation,
21/// publication and claim records each write keeps beside its rows. A write records no
22/// change to them, and recording one costs as much as an app row's (§4); a
23/// live query that read one would be a bug coven refuses at once.
24const 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
50/// Whether a write records its changes to `table` for live queries.
51pub(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    /// The rows whose changes matter.
60    pub(crate) scope: RowScope,
61    /// Only inserted rows affect this read, not updates or deletions.
62    pub(crate) inserted_only: bool,
63}
64
65pub(crate) struct RowChange {
66    pub(crate) table: String,
67    pub(crate) column: String,
68    /// The row before and after, `None` on the side where it isn't there;
69    /// `None` altogether for a change no row names, as a virtual table's or
70    /// one only the update hook observed, which every scope covers.
71    pub(crate) rows: Option<(Option<RowImage>, Option<RowImage>)>,
72}
73
74impl TableRead {
75    /// Whether `change` could change what this read returned, running
76    /// `lookups` for a join's values.
77    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
96/// An insertion: a whole-row change with no row before it.
97fn inserted(change: &RowChange) -> bool {
98    change.column.is_empty() && change.rows.as_ref().is_some_and(|(old, _)| old.is_none())
99}
100
101/// Why this handle refuses every call until the store is reopened.
102#[derive(Clone)]
103pub(crate) enum HandleFailure {
104    /// Saving pending state or the sync status failed, with this cause.
105    State(Arc<DbError>),
106    /// Rolling back a failed transaction failed, so the writer may still hold
107    /// its uncommitted changes.
108    Rollback,
109    /// A WAL checkpoint failed with this cause, other than on a busy or
110    /// locked database, and the store's checkpointing stopped.
111    Checkpoint(Arc<DbError>),
112    /// Checking or gathering the planner's statistics failed with this
113    /// cause, other than on a busy or locked database, and keeping them
114    /// stopped.
115    Statistics(Arc<DbError>),
116    /// The handle's clock could not wait for an invite's expiry (§13.2).
117    WallTimer(coven_foundation::clock::WallTimerError),
118    /// A bug, a panic or a fault a drop found, may have left this store's
119    /// state wrong (E15). It reached its one reporter as it happened, so
120    /// the calls after it repeat it as `DbError::StoreFailed`.
121    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    /// Whether the store has closed.
146    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    /// A run whose reads aren't known until it ends: the commits made
160    /// since it began, kept to judge then, and the reads of the run before
161    /// it, whose scopes name the values those commits record (§4).
162    Reading {
163        commits: Vec<Arc<Vec<RowChange>>>,
164        previous: Arc<ReadSet>,
165    },
166    Waiting(Arc<ReadSet>),
167}
168
169impl Tracking {
170    /// The reads whose scopes a write records values for: those waited on,
171    /// or, while a run reads, its previous run's, which a run with the
172    /// same statements tests again.
173    fn scoped(&self) -> &Arc<ReadSet> {
174        match self {
175            Self::Reading { previous: reads, .. } | Self::Waiting(reads) => reads,
176        }
177    }
178}
179
180/// A subscription's tracking, and how many times it began or finished a
181/// run: a write judged against one count's reads can't stand for another's.
182struct Tracked {
183    tracking: Tracking,
184    epoch: u64,
185}
186
187struct SubscriptionState {
188    tracking: Mutex<Tracked>,
189    state: watch::Sender<CommitState>,
190    /// Commits made within this scope are not observed.
191    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    /// Start keeping the commits a run meets, until it finishes. Meanwhile
225    /// a write records the values the scopes this subscription went by
226    /// until now test.
227    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    /// Wait on nothing, whatever this subscription was doing: its query
236    /// ended on a bug, so what its run read is unknown (E15).
237    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    /// Wait on `reads`, which join no table: no commit needs a lookup.
244    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    /// Wait on `reads`, a run's. The commits made while it ran are judged
252    /// against them, with `lookups` reading the run's own snapshot: a value
253    /// matches a join there exactly when the run's partners held it. A
254    /// lookup that fails marks the query to run again, and its error is
255    /// the run's.
256    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            // Judge without the lock, so writes go on committing; those that
270            // commit meanwhile are judged on the next pass.
271            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
301/// Whether `changes` could change what `reads` returned. Past
302/// `ROWS_PER_TABLE` row changes to one table, a join's values in that table
303/// count as matched without a lookup, as a table changed whole would, so a
304/// write's lookups stay bounded.
305fn 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/// The lookups one judging ran, by lookup and value, so rows sharing a
334/// value look it up once.
335#[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/// A value as a hash key, by its type and bytes.
356#[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
377/// A write's changes, and how each live query that waited on known reads
378/// judged them in the write's transaction, before it committed.
379pub(crate) struct Judged {
380    changes: Arc<Vec<RowChange>>,
381    /// Each subscription judged, at its epoch then, and whether the changes
382    /// affect it.
383    verdicts: Vec<(Weak<SubscriptionState>, u64, bool)>,
384}
385
386impl Judged {
387    /// Changes no live query judged, as those that name no row: each
388    /// waiting query judges them as they reach it, without lookups.
389    pub(crate) fn unjudged(changes: Vec<RowChange>) -> Self {
390        Self { changes: Arc::new(changes), verdicts: Vec::new() }
391    }
392}
393
394/// Every commit made outside one attempt since it began, kept until the
395/// attempt names the rows its wait depends on.
396pub 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    /// The store call this thread runs, if any, and the bug its own work
408    /// failed that store's handle with (E15).
409    // Clippy asks for a `const` initializer this already has where the
410    // target has no native thread locals, as Android: std builds those
411    // from a function, which the lint reads as the initializer.
412    #[cfg_attr(target_os = "android", allow(clippy::missing_const_for_thread_local))]
413    static CALL: RefCell<Option<Call>> = const { RefCell::new(None) };
414}
415
416/// One of a handle's calls running on this thread.
417struct Call {
418    /// The handle the call is on, by its state's address.
419    handle: usize,
420    /// The first bug the call's work failed the handle with, which the call
421    /// returns as its own.
422    bug: Option<Bug>,
423}
424
425/// The bug of `what`, a lock's state, which a panic while it was held may
426/// have left half-changed.
427#[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    /// Fail every later call and subscription; the first failure is kept.
434    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    /// Where a bug that fails the handle outside its calls goes (E15).
467    bugs: BugSinkRef,
468    /// How many more of the store's upkeep threads start before the next is
469    /// refused, when a test asked for that.
470    #[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    /// Let `started` upkeep threads start, if given, and refuse the next.
489    #[cfg(test)]
490    pub(crate) fn refuse_upkeep_thread_after(&self, started: Option<usize>) {
491        *lock_unpoisoned(&self.upkeep_threads) = started;
492    }
493
494    /// Whether the system refuses the upkeep thread starting now.
495    #[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    /// Observe every commit except those made within `outside`. Once the
513    /// store has closed, a new subscription reports that it closed, as a
514    /// call to the store does, not the failure it may have closed with.
515    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    /// Watch `reads` from now on; `first` reports the state now as their
526    /// first change.
527    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    /// Keep every commit made from now on outside `attempt`, until the
535    /// attempt names the rows it waits on (`watch_attempt`).
536    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    /// Watch `rows` after `attempt`: a commit since it began that changed
543    /// one is reported at once, as evidence against the wait it ended in.
544    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    /// Judge a write's `changes`, made within `scope`, for every live query
555    /// waiting on known reads that isn't already marked to run again, with
556    /// `lookups` reading the write's transaction before it commits (§4).
557    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    /// Report a commit made within `scope`, if any. A query still waiting on
591    /// the reads it was judged against takes its verdict. One running keeps
592    /// the changes, to judge against its run's snapshot as it finishes. One
593    /// that finished another run since, or wasn't judged, judges the
594    /// changes counting every join's value as matched: their transaction
595    /// has ended, so no lookup can read it.
596    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    /// The columns some subscription's scope tests, by table, all
631    /// lowercase: a waiting one's, or a running one's previous run's. A
632    /// write records those values of the rows it changes there. A change to
633    /// any other table needs no values to wake the subscriptions that read
634    /// it.
635    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    /// What every call on the handle fails with: its failure, or
649    /// `StoreClosed` once it closed.
650    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    /// Fail every later call and subscription; the first failure is kept.
661    pub(crate) fn fail(&self, failure: HandleFailure) {
662        self.with_state(|state| state.fail(failure));
663    }
664
665    /// Fail the handle with `bug`, a panic in work that may have held its
666    /// state or a fault a drop found in it. The handle fails with this bug,
667    /// not the one a poisoned lock would name. The bug goes to one
668    /// reporter: the handle's call this thread runs, which returns it as
669    /// its own (`calling`), or the store's bug sink, outside such a call or
670    /// once the call has a bug of its own already (E15).
671    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        // A drop as this thread ends finds its locals gone: no call runs.
681        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    /// Run `call`, one of this handle's calls, on this thread. Beside its
696    /// result comes the bug its own work failed the handle with, if any,
697    /// which the call returns as its own (E15). A call that unwinds leaves
698    /// that bug to the sink: its panic is the call's bug then. A call made
699    /// inside another on the same handle is part of it, and leaves its bug
700    /// to the outer one, which the app hears from.
701    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        /// Restores the call this one runs inside, if any, as it ends.
709        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    /// Hand `bug`, which no call returns, to the store's bug sink (E15).
731    pub(crate) fn report(&self, bug: &Bug) {
732        coven_foundation::task::report(self.bugs.as_ref(), bug);
733    }
734
735    /// This handle, as `CALL` names it.
736    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    /// Panic while holding the handle's state, as a bug in judging a commit
745    /// for live queries would.
746    #[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    /// Run `change` on the handle's state. A panic while it was held may
758    /// have left some subscription untold of a commit, so the handle fails
759    /// with that bug, which tells every subscription; the failure then
760    /// stands in for the poisoning (E15).
761    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;