Skip to main content

coven/
live_query.rs

1use std::sync::{Arc, Mutex};
2
3use crate::{CovenError, CovenResult, SqlReadContext};
4use coven_database::store::StoreReads;
5use coven_database::{QueryDependencies, StoreRowWrites};
6
7type Query<T> =
8    dyn for<'connection> Fn(SqlReadContext<'connection>) -> CovenResult<T> + Send + Sync;
9type QueryOutcome<Value> = CovenResult<(CovenResult<Value>, QueryDependencies)>;
10type RequestedQuery<Request, Value> = dyn Fn(
11        StoreReads,
12        Request,
13    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = QueryOutcome<Value>> + Send>>
14    + Send
15    + Sync;
16
17/// Identifies an absolute request accepted by a reconfigurable live query.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
19pub struct LiveQueryRevision(u64);
20
21impl LiveQueryRevision {
22    /// Return this revision as its monotonically increasing integer value.
23    pub fn get(self) -> u64 {
24        self.0
25    }
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
29#[error("the live query subscription is closed")]
30pub struct LiveQueryClosed;
31
32#[derive(Clone)]
33struct RequestState<Request> {
34    revision: LiveQueryRevision,
35    request: Request,
36}
37
38enum PendingRun {
39    Initial,
40    Triggered {
41        request_changed: bool,
42        commits: Vec<Arc<coven_database::CommittedChanges>>,
43        unknown_commit: bool,
44        previous_dependencies_matched: bool,
45    },
46}
47
48impl PendingRun {
49    fn request_changed(&mut self) {
50        match self {
51            Self::Initial => {
52                *self = Self::Triggered {
53                    request_changed: true,
54                    commits: Vec::new(),
55                    unknown_commit: false,
56                    previous_dependencies_matched: false,
57                };
58            }
59            Self::Triggered {
60                request_changed, ..
61            } => *request_changed = true,
62        }
63    }
64
65    fn committed(
66        &mut self,
67        changes: Arc<coven_database::CommittedChanges>,
68        dependencies: &QueryDependencies,
69    ) {
70        if let Self::Triggered {
71            commits,
72            previous_dependencies_matched,
73            ..
74        } = self
75        {
76            *previous_dependencies_matched |= dependencies.is_affected_by(&changes);
77            commits.push(changes);
78        }
79    }
80
81    fn lagged(&mut self) {
82        if let Self::Triggered { unknown_commit, .. } = self {
83            *unknown_commit = true;
84        }
85    }
86
87    fn cause(&self, dependencies: &QueryDependencies) -> ReconfigurableLiveQueryCause {
88        let Self::Triggered {
89            request_changed,
90            commits,
91            unknown_commit,
92            previous_dependencies_matched,
93        } = self
94        else {
95            return ReconfigurableLiveQueryCause::Initial;
96        };
97        let database_changed = *unknown_commit
98            || commits
99                .iter()
100                .any(|changes| dependencies.is_affected_by(changes))
101            || *previous_dependencies_matched;
102        match (*request_changed, database_changed) {
103            (true, true) => ReconfigurableLiveQueryCause::RequestAndDatabaseChanged,
104            (true, false) => ReconfigurableLiveQueryCause::RequestChanged,
105            (false, true) => ReconfigurableLiveQueryCause::DatabaseChanged,
106            (false, false) => {
107                unreachable!("a triggered live query must have a request or database cause")
108            }
109        }
110    }
111}
112
113/// Changes the absolute request evaluated by a reconfigurable live query.
114#[derive(Clone)]
115pub struct LiveQueryRequests<Request> {
116    state: Arc<Mutex<RequestState<Request>>>,
117    sender: tokio::sync::watch::Sender<RequestState<Request>>,
118}
119
120/// Why a reconfigurable live query produced an event.
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub enum ReconfigurableLiveQueryCause {
123    /// The subscription's first result.
124    Initial,
125    /// The absolute request changed.
126    RequestChanged,
127    /// A relevant database commit occurred.
128    DatabaseChanged,
129    /// The request changed and a relevant database commit occurred before the run.
130    RequestAndDatabaseChanged,
131}
132
133impl<Request> LiveQueryRequests<Request>
134where
135    Request: Clone + PartialEq,
136{
137    /// Replace the requested value and return the revision that will deliver it.
138    ///
139    /// Repeating the current request returns its existing revision. The call
140    /// fails after the subscription has been dropped.
141    pub fn set(&self, request: Request) -> Result<LiveQueryRevision, LiveQueryClosed> {
142        if self.sender.receiver_count() == 0 {
143            return Err(LiveQueryClosed);
144        }
145        let mut state = self
146            .state
147            .lock()
148            .expect("live query request mutex poisoned");
149        if state.request == request {
150            return Ok(state.revision);
151        }
152        state.revision = LiveQueryRevision(
153            state
154                .revision
155                .0
156                .checked_add(1)
157                .expect("live query request revision overflow"),
158        );
159        state.request = request;
160        self.sender
161            .send(state.clone())
162            .map_err(|_| LiveQueryClosed)?;
163        Ok(state.revision)
164    }
165}
166
167/// One query result and the exact request used to produce it.
168pub struct ReconfigurableLiveQueryEvent<Request, Value> {
169    cause: ReconfigurableLiveQueryCause,
170    state: RequestState<Request>,
171    result: CovenResult<Value>,
172}
173
174impl<Request, Value> ReconfigurableLiveQueryEvent<Request, Value> {
175    /// Return why this event was produced.
176    pub fn cause(&self) -> ReconfigurableLiveQueryCause {
177        self.cause
178    }
179
180    /// Return the revision of the request used for this result.
181    pub fn revision(&self) -> LiveQueryRevision {
182        self.state.revision
183    }
184
185    /// Return the absolute request used for this result.
186    pub fn request(&self) -> &Request {
187        &self.state.request
188    }
189
190    /// Return the query result.
191    pub fn into_result(self) -> CovenResult<Value> {
192        self.result
193    }
194}
195
196/// A tracked query whose absolute request can change without replacing the
197/// subscription.
198///
199/// Construct one with
200/// [`CovenHandle::subscribe_reconfigurable`](crate::CovenHandle::subscribe_reconfigurable).
201/// Request changes and relevant commits are coalesced before each run. If the
202/// request changes while a run is in progress, that result is discarded and
203/// the latest request is evaluated before an event is returned.
204///
205/// A run caused only by committed changes whose value equals the last
206/// delivered value is not delivered: the query's read dependencies are
207/// table-and-key granular, so a paged query (`ORDER BY … LIMIT`) reruns for any
208/// row change in its tables, and most of those reruns leave its window
209/// unchanged. The initial run, a run after a request change, and every error
210/// are always delivered; the first successful value after an error is too.
211pub struct ReconfigurableLiveQuery<Request, Value> {
212    _writer: StoreRowWrites,
213    reader: StoreReads,
214    changes: tokio::sync::broadcast::Receiver<Arc<coven_database::CommittedChanges>>,
215    dependencies: QueryDependencies,
216    pending: Option<PendingRun>,
217    query: Arc<RequestedQuery<Request, Value>>,
218    request_receiver: tokio::sync::watch::Receiver<RequestState<Request>>,
219    requests: LiveQueryRequests<Request>,
220    current: RequestState<Request>,
221    /// The value most recently delivered by `next`, for skipping commit-caused
222    /// reruns that produce it again. `None` before the first delivery and after
223    /// a delivered error.
224    last_delivered: Option<Value>,
225}
226
227impl<Request, Value> ReconfigurableLiveQuery<Request, Value>
228where
229    Request: Clone + PartialEq + Send + Sync + 'static,
230    Value: Send + 'static,
231{
232    pub(crate) fn new<F>(
233        writer: StoreRowWrites,
234        reader: StoreReads,
235        initial_request: Request,
236        query: F,
237    ) -> Self
238    where
239        F: for<'connection> Fn(&Request, SqlReadContext<'connection>) -> CovenResult<Value>
240            + Send
241            + Sync
242            + 'static,
243    {
244        let query = Arc::new(query);
245        let changes = writer.subscribe_committed_changes();
246        let current = RequestState {
247            revision: LiveQueryRevision(0),
248            request: initial_request,
249        };
250        let state = Arc::new(Mutex::new(current.clone()));
251        let (sender, request_receiver) = tokio::sync::watch::channel(current.clone());
252        Self {
253            _writer: writer,
254            reader,
255            changes,
256            dependencies: QueryDependencies::unknown(),
257            pending: Some(PendingRun::Initial),
258            query: Arc::new(move |reader, request| {
259                let query = query.clone();
260                Box::pin(async move {
261                    reader
262                        .read_tracked(move |sql| query(&request, sql))
263                        .await
264                        .map_err(CovenError::from)
265                })
266            }),
267            request_receiver,
268            requests: LiveQueryRequests { state, sender },
269            current,
270            last_delivered: None,
271        }
272    }
273
274    /// Process each fetched value on bounded workers after releasing its read
275    /// connection. Dependencies and request revisions stay attached to the read;
276    /// only processed values are compared when deciding whether to deliver.
277    ///
278    /// The returned subscription starts with an initial result for the latest
279    /// request, even if this subscription previously delivered values. Existing
280    /// request handles continue to control it.
281    pub fn process<P, R>(self, process: P) -> ReconfigurableLiveQuery<Request, R>
282    where
283        P: Fn(&Request, Value) -> CovenResult<R> + Send + Sync + 'static,
284        R: Send + 'static,
285    {
286        let query = self.query;
287        let process = Arc::new(process);
288        ReconfigurableLiveQuery {
289            _writer: self._writer,
290            reader: self.reader,
291            changes: self.changes,
292            dependencies: QueryDependencies::unknown(),
293            pending: Some(PendingRun::Initial),
294            query: Arc::new(move |reader, request| {
295                let query = query.clone();
296                let process = process.clone();
297                Box::pin(async move {
298                    let (result, dependencies) = query(reader.clone(), request.clone()).await?;
299                    let result = match result {
300                        Ok(raw) => reader.process(move || process(&request, raw)).await,
301                        Err(error) => Err(error),
302                    };
303                    Ok((result, dependencies))
304                })
305            }),
306            request_receiver: self.request_receiver,
307            requests: self.requests,
308            current: self.current,
309            last_delivered: None,
310        }
311    }
312
313    /// Return a handle that can replace this subscription's absolute request.
314    pub fn requests(&self) -> LiveQueryRequests<Request> {
315        self.requests.clone()
316    }
317
318    /// Return the initial event, or wait for a request change or relevant
319    /// committed database change and return the next event.
320    ///
321    /// Query errors are events and do not end the subscription. Cancelling the
322    /// future preserves the pending request or database change. A commit-caused
323    /// rerun whose value equals the last delivered value is not an event; the
324    /// query goes back to waiting.
325    pub async fn next(&mut self) -> ReconfigurableLiveQueryEvent<Request, Value>
326    where
327        Value: Clone + PartialEq,
328    {
329        loop {
330            self.await_pending().await;
331            self.drain_pending();
332            if let Some(event) = self.run().await {
333                return event;
334            }
335        }
336    }
337
338    /// Evaluate the pending run. `None` means the run produced the value
339    /// already delivered and no event is due.
340    async fn run(&mut self) -> Option<ReconfigurableLiveQueryEvent<Request, Value>>
341    where
342        Value: Clone + PartialEq,
343    {
344        loop {
345            let state = self.current.clone();
346            let query = self.query.clone();
347            let request = state.request.clone();
348            let outcome = query(self.reader.clone(), request).await;
349
350            if self.request_receiver.has_changed().unwrap_or(false) {
351                self.pending
352                    .as_mut()
353                    .expect("live query run is pending")
354                    .request_changed();
355                self.accept_latest_request();
356                self.drain_pending();
357                continue;
358            }
359
360            let result = match outcome {
361                Ok((result, dependencies)) => {
362                    self.dependencies = dependencies;
363                    result
364                }
365                Err(error) => {
366                    self.dependencies = QueryDependencies::unknown();
367                    Err(error)
368                }
369            };
370            let pending = self.pending.take().expect("live query run is pending");
371            let cause = pending.cause(&self.dependencies);
372            if cause == ReconfigurableLiveQueryCause::DatabaseChanged
373                && matches!((&result, &self.last_delivered), (Ok(value), Some(last)) if value == last)
374            {
375                return None;
376            }
377            self.last_delivered = result.as_ref().ok().cloned();
378            return Some(ReconfigurableLiveQueryEvent {
379                cause,
380                state,
381                result,
382            });
383        }
384    }
385
386    async fn await_pending(&mut self) {
387        while self.pending.is_none() {
388            tokio::select! {
389                changed = self.request_receiver.changed() => {
390                    changed.expect("the live query retains its request sender");
391                    self.accept_latest_request();
392                    self.pending = Some(PendingRun::Triggered {
393                        request_changed: true,
394                        commits: Vec::new(),
395                        unknown_commit: false,
396                        previous_dependencies_matched: false,
397                    });
398                }
399                changes = self.changes.recv() => {
400                    match changes {
401                        Ok(changes) if self.dependencies.is_affected_by(&changes) => {
402                            self.pending = Some(PendingRun::Triggered {
403                                request_changed: false,
404                                commits: vec![changes],
405                                unknown_commit: false,
406                                previous_dependencies_matched: true,
407                            });
408                        }
409                        Ok(_) => {}
410                        Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {
411                            self.pending = Some(PendingRun::Triggered {
412                                request_changed: false,
413                                commits: Vec::new(),
414                                unknown_commit: true,
415                                previous_dependencies_matched: false,
416                            });
417                        }
418                        Err(tokio::sync::broadcast::error::RecvError::Closed) => {
419                            panic!("the live query retains its committed-change sender")
420                        }
421                    }
422                }
423            }
424        }
425    }
426
427    fn drain_pending(&mut self) {
428        if self.request_receiver.has_changed().unwrap_or(false) {
429            self.accept_latest_request();
430            self.pending
431                .as_mut()
432                .expect("live query run is pending")
433                .request_changed();
434        }
435        loop {
436            match self.changes.try_recv() {
437                Ok(changes) => self
438                    .pending
439                    .as_mut()
440                    .expect("live query run is pending")
441                    .committed(changes, &self.dependencies),
442                Err(tokio::sync::broadcast::error::TryRecvError::Lagged(_)) => self
443                    .pending
444                    .as_mut()
445                    .expect("live query run is pending")
446                    .lagged(),
447                Err(tokio::sync::broadcast::error::TryRecvError::Empty) => break,
448                Err(tokio::sync::broadcast::error::TryRecvError::Closed) => {
449                    panic!("the live query retains its committed-change sender")
450                }
451            }
452        }
453    }
454
455    fn accept_latest_request(&mut self) {
456        self.current = self.request_receiver.borrow_and_update().clone();
457    }
458}
459
460/// A query over the store's reader that runs initially and whenever a committed
461/// row change can affect its result.
462///
463/// Construct one with [`CovenHandle::subscribe`](crate::CovenHandle::subscribe),
464/// then call [`next`](Self::next) for the initial value and each later value.
465/// Query errors are values in the sequence and do not end the subscription.
466pub struct LiveQuery<T> {
467    inner: ReconfigurableLiveQuery<(), T>,
468}
469
470impl<T> LiveQuery<T>
471where
472    T: Send + 'static,
473{
474    pub(crate) fn new<F>(writer: StoreRowWrites, reader: StoreReads, query: F) -> Self
475    where
476        F: for<'connection> Fn(SqlReadContext<'connection>) -> CovenResult<T>
477            + Send
478            + Sync
479            + 'static,
480    {
481        let query: Arc<Query<T>> = Arc::new(query);
482        Self {
483            inner: ReconfigurableLiveQuery::new(writer, reader, (), move |(), sql| query(sql)),
484        }
485    }
486
487    /// Process each fetched value after releasing the database connection.
488    /// Only the processed result needs to implement `Clone` and `PartialEq`
489    /// to deliver values through [`next`](Self::next).
490    ///
491    /// The returned subscription delivers an initial processed result even if
492    /// this subscription previously delivered unprocessed values.
493    pub fn process<P, R>(self, process: P) -> LiveQuery<R>
494    where
495        P: Fn(T) -> CovenResult<R> + Send + Sync + 'static,
496        R: Send + 'static,
497    {
498        LiveQuery {
499            inner: self.inner.process(move |(), raw| process(raw)),
500        }
501    }
502
503    /// Return the query's initial value, or wait for a committed change that can
504    /// affect it and return the value after that commit. A commit whose rerun
505    /// produces the value already returned is not a value; the wait continues.
506    pub async fn next(&mut self) -> CovenResult<T>
507    where
508        T: Clone + PartialEq,
509    {
510        self.inner.next().await.into_result()
511    }
512}