Rindle docs and package mapSkip to main content

rindle_replica/
progress.rs

1//! **Connection progress + the poke rule** (`OPTIMISTIC-WRITES-DESIGN.md` §8.3–§8.4).
2//!
3//! The server-side bookkeeping that turns a stream of commits into per-connection
4//! [`ProgressFrame`]s. Per connection it tracks the live query set; per query, the
5//! highest commit version (`cv`) through which its result is **known-current**.
6//!
7//! **The poke rule (§8.4):** a connection is poked on a commit only when one of *its*
8//! queries' results actually changed. Mutation confirmation needs no extra rule:
9//! `lmid` is a row in [`crate::CLIENT_MUTATIONS_TABLE`], so a client's own lmid
10//! advance changes its one-row system query — an ordinary data poke, released by the
11//! same `cv_min` as the commit's effects. Irrelevant writes produce no frame — a
12//! read-only/idle client sees zero extra traffic; `cv_min` advancing in the
13//! background is invisible to a client that doesn't care.
14//!
15//! **`cv_min` is server-computed (§8.3)** because per-query silence is ambiguous to the
16//! client (current-and-unchanged vs. not-yet-processed). This tracker is the
17//! single-thread [`Db`](crate::Db) instantiation of the rule: every registered query is
18//! processed synchronously inside each commit, so [`note_commit`](ProgressTracker::note_commit)
19//! advances **every** query's known-current `cv` — the degenerate footprint check
20//! (untouched ⇒ trivially current). The multi-core `Cluster` drives the same rule from
21//! worker positions instead (the drain).
22//!
23//! The *standalone* quiet-window progress frame (§8.4) is a `Cluster`-era concern: on
24//! the single-thread path `cv_min` only moves when a commit happens, and every commit
25//! that matters to a connection already pokes it.
26
27use std::collections::{BTreeSet, HashMap};
28
29use crate::normalize_protocol::ProgressFrame;
30use crate::QueryId;
31
32/// Per-connection state: its live queries.
33struct Conn {
34    queries: BTreeSet<QueryId>,
35}
36
37/// See the module docs. Single-thread (`Db`) semantics; one instance per server.
38pub struct ProgressTracker {
39    /// The latest commit version noted (the replica's committed watermark).
40    frontier: u64,
41    /// Known-current `cv` per live query.
42    queries: HashMap<QueryId, u64>,
43    /// Connections, keyed by the caller's opaque connection id.
44    conns: HashMap<u64, Conn>,
45}
46
47impl ProgressTracker {
48    /// Open at the replica's current committed watermark
49    /// ([`Db::committed_tx_id`](crate::Db::committed_tx_id)).
50    pub fn new(initial_cv: u64) -> ProgressTracker {
51        ProgressTracker {
52            frontier: initial_cv,
53            queries: HashMap::new(),
54            conns: HashMap::new(),
55        }
56    }
57
58    /// Register a connection under the caller's opaque id.
59    pub fn connect(&mut self, conn: u64) {
60        self.conns.insert(
61            conn,
62            Conn {
63                queries: BTreeSet::new(),
64            },
65        );
66    }
67
68    /// Drop a connection and its query registrations.
69    pub fn disconnect(&mut self, conn: u64) {
70        if let Some(c) = self.conns.remove(&conn) {
71            for q in c.queries {
72                self.queries.remove(&q);
73            }
74        }
75    }
76
77    /// Register a live query on `conn`, known-current as of its hydrate watermark.
78    pub fn add_query(&mut self, conn: u64, query: QueryId, hydrated_cv: u64) {
79        if let Some(c) = self.conns.get_mut(&conn) {
80            c.queries.insert(query);
81            self.queries.insert(query, hydrated_cv);
82        }
83    }
84
85    /// Deregister a query (unsubscribed / destroyed) so it no longer pins `cv_min`.
86    pub fn remove_query(&mut self, conn: u64, query: QueryId) {
87        if let Some(c) = self.conns.get_mut(&conn) {
88            c.queries.remove(&query);
89        }
90        self.queries.remove(&query);
91    }
92
93    /// Fold one committed transaction in: `cv` (the commit's `TxId`) and the queries
94    /// whose results changed (the subscriptions that fired — a client's own lmid
95    /// advance fires its system query, so it is in here like any data change).
96    /// Advances every query's known-current `cv` (single-thread semantics — see module
97    /// docs) and returns the poke set: `(conn, frame)` for exactly the connections the
98    /// poke rule selects, in connection-id order.
99    pub fn note_commit(&mut self, cv: u64, changed: &[QueryId]) -> Vec<(u64, ProgressFrame)> {
100        self.frontier = self.frontier.max(cv);
101        for known in self.queries.values_mut() {
102            *known = (*known).max(cv);
103        }
104
105        let changed: BTreeSet<QueryId> = changed.iter().copied().collect();
106        let mut conn_ids: Vec<u64> = self.conns.keys().copied().collect();
107        conn_ids.sort_unstable();
108
109        let mut pokes = Vec::new();
110        for id in conn_ids {
111            let conn = &self.conns[&id];
112            let data_changed = conn.queries.iter().any(|q| changed.contains(q));
113            if !data_changed {
114                continue; // the poke rule: this commit is invisible to this connection
115            }
116            let cv_min = conn_cv_min(&self.queries, self.frontier, conn);
117            pokes.push((id, ProgressFrame { cv_min }));
118        }
119        pokes
120    }
121
122    /// The commit version every one of `conn`'s live queries has processed through
123    /// (the frame's `cv_min`); the frontier for a query-less connection.
124    pub fn cv_min(&self, conn: u64) -> Option<u64> {
125        self.conns
126            .get(&conn)
127            .map(|c| conn_cv_min(&self.queries, self.frontier, c))
128    }
129}
130
131fn conn_cv_min(queries: &HashMap<QueryId, u64>, frontier: u64, conn: &Conn) -> u64 {
132    conn.queries
133        .iter()
134        .map(|q| queries.get(q).copied().unwrap_or(frontier))
135        .min()
136        .unwrap_or(frontier)
137}