klukai_types/
updates.rs

1#![allow(clippy::result_large_err)]
2
3use crate::actor::ActorId;
4use crate::agent::SplitPool;
5use crate::api::sqlite::ChangeType;
6use crate::api::{ColumnName, NotifyEvent, SqliteValueRef, TableName};
7use crate::base::CrsqlDbVersion;
8use crate::change::Change;
9use crate::pubsub::{MatchCandidates, MatchableChange, MatcherError, unpack_columns};
10use crate::schema::Schema;
11use crate::spawn::spawn_counted;
12use crate::tripwire::Tripwire;
13use antithesis_sdk::assert_sometimes;
14use indexmap::{IndexMap, map::Entry};
15use metrics::{Counter, counter, histogram};
16use parking_lot::RwLock;
17use rusqlite::Connection;
18use std::collections::BTreeMap;
19use std::fmt::{Debug, Formatter};
20use std::sync::Arc;
21use std::time::Duration;
22use tokio::sync::mpsc;
23use tokio::task::block_in_place;
24use tokio::time::Instant;
25use tokio_util::sync::{CancellationToken, WaitForCancellationFuture};
26use tracing::{debug, error, info, trace, warn};
27use uuid::Uuid;
28
29pub trait Manager<H> {
30    fn trait_type(&self) -> String;
31    fn get(&self, id: &Uuid) -> Option<H>;
32    fn remove(&self, id: &Uuid) -> Option<H>;
33    fn get_handles(&self) -> BTreeMap<Uuid, H>;
34}
35
36pub trait Handle {
37    fn id(&self) -> Uuid;
38    fn cancelled(&self) -> WaitForCancellationFuture<'_>;
39    fn filter_matchable_change(
40        &self,
41        candidates: &mut MatchCandidates,
42        change: MatchableChange,
43    ) -> bool;
44    fn changes_tx(&self) -> mpsc::Sender<MatchCandidates>;
45    fn cleanup(&self) -> impl std::future::Future<Output = ()> + Send;
46    fn get_counter(&self, table: &str) -> &HandleMetrics;
47}
48
49#[derive(Clone)]
50pub struct HandleMetrics {
51    pub matched_count: Counter,
52}
53
54impl std::fmt::Debug for HandleMetrics {
55    fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
56        f.debug_struct("Counter").finish_non_exhaustive()
57    }
58}
59
60#[derive(Default, Debug, Clone)]
61pub struct UpdatesManager(Arc<RwLock<InnerUpdatesManager>>);
62
63impl Manager<UpdateHandle> for UpdatesManager {
64    fn trait_type(&self) -> String {
65        "updates".to_string()
66    }
67
68    fn get(&self, id: &Uuid) -> Option<UpdateHandle> {
69        self.0.read().get(id)
70    }
71
72    fn remove(&self, id: &Uuid) -> Option<UpdateHandle> {
73        let mut inner = self.0.write();
74        inner.remove(id)
75    }
76
77    fn get_handles(&self) -> BTreeMap<Uuid, UpdateHandle> {
78        self.0.read().handles.clone()
79    }
80}
81
82impl Handle for UpdateHandle {
83    fn id(&self) -> Uuid {
84        self.inner.id
85    }
86
87    fn cancelled(&self) -> WaitForCancellationFuture<'_> {
88        self.inner.cancel.cancelled()
89    }
90
91    fn filter_matchable_change(
92        &self,
93        candidates: &mut MatchCandidates,
94        change: MatchableChange,
95    ) -> bool {
96        if change.table.to_string() != self.inner.name {
97            return false;
98        }
99
100        trace!("filtering change {change:?}");
101        // don't double process the same pk
102        if candidates
103            .get(change.table)
104            .map(|pks| pks.contains_key(change.pk))
105            .unwrap_or_default()
106        {
107            trace!("already contained key");
108            return false;
109        }
110
111        if let Some(v) = candidates.get_mut(change.table) {
112            v.insert(change.pk.into(), change.cl).is_none()
113        } else {
114            candidates.insert(
115                change.table.clone(),
116                [(change.pk.to_vec(), change.cl)].into(),
117            );
118            true
119        }
120    }
121
122    fn changes_tx(&self) -> mpsc::Sender<MatchCandidates> {
123        self.inner.changes_tx.clone()
124    }
125
126    fn cleanup(&self) -> impl std::future::Future<Output = ()> + Send {
127        let cancel = self.inner.cancel.clone();
128        let id = self.inner.id;
129
130        async move {
131            cancel.cancel();
132            info!(sub_id = %id, "Canceled subscription");
133        }
134    }
135
136    fn get_counter(&self, table: &str) -> &HandleMetrics {
137        // this should not happen
138        if table != self.inner.name {
139            warn!(update_tbl = %self.inner.name, "udpates handle get_counter method called for wrong table : {table}! This shouldn't happen!")
140        }
141        &self.inner.counters
142    }
143}
144
145const UPDATE_EVENT_CHANNEL_CAP: usize = 512;
146
147// tools to bootstrap a new notifier
148pub struct UpdateCreated {
149    pub evt_rx: mpsc::Receiver<NotifyEvent>,
150}
151
152#[derive(Debug, Default)]
153struct InnerUpdatesManager {
154    tables: BTreeMap<String, Uuid>,
155    handles: BTreeMap<Uuid, UpdateHandle>,
156}
157
158impl InnerUpdatesManager {
159    fn remove(&mut self, id: &Uuid) -> Option<UpdateHandle> {
160        let handle = self.handles.remove(id)?;
161        Some(handle)
162    }
163
164    fn get(&self, id: &Uuid) -> Option<UpdateHandle> {
165        self.handles.get(id).cloned()
166    }
167}
168
169impl UpdatesManager {
170    pub fn get(&self, table: &str) -> Option<UpdateHandle> {
171        let id = self.0.read().tables.get(table).cloned();
172        if let Some(id) = id {
173            return self.0.read().handles.get(&id).cloned();
174        }
175        None
176    }
177
178    pub fn get_or_insert(
179        &self,
180        tbl_name: &str,
181        schema: &Schema,
182        _pool: &SplitPool,
183        tripwire: Tripwire,
184    ) -> Result<(UpdateHandle, Option<UpdateCreated>), MatcherError> {
185        if let Some(handle) = self.get(tbl_name) {
186            return Ok((handle, None));
187        }
188
189        let mut inner = self.0.write();
190        let (evt_tx, evt_rx) = mpsc::channel(UPDATE_EVENT_CHANNEL_CAP);
191
192        let id = Uuid::new_v4();
193        let handle_res = UpdateHandle::create(id, tbl_name, schema, evt_tx, tripwire);
194
195        let handle = match handle_res {
196            Ok(handle) => handle,
197            Err(e) => {
198                return Err(e);
199            }
200        };
201
202        inner.handles.insert(id, handle.clone());
203        inner.tables.insert(tbl_name.to_string(), id);
204
205        Ok((handle, Some(UpdateCreated { evt_rx })))
206    }
207
208    pub fn remove(&self, id: &Uuid) -> Option<UpdateHandle> {
209        let mut inner = self.0.write();
210
211        inner.remove(id)
212    }
213}
214
215#[derive(Clone, Debug)]
216pub struct UpdateHandle {
217    inner: Arc<InnerUpdateHandle>,
218}
219
220#[derive(Clone, Debug)]
221pub struct InnerUpdateHandle {
222    id: Uuid,
223    name: String,
224    cancel: CancellationToken,
225    changes_tx: mpsc::Sender<MatchCandidates>,
226    counters: HandleMetrics,
227}
228
229impl UpdateHandle {
230    pub fn id(&self) -> Uuid {
231        self.inner.id
232    }
233
234    pub fn create(
235        id: Uuid,
236        tbl_name: &str,
237        schema: &Schema,
238        evt_tx: mpsc::Sender<NotifyEvent>,
239        tripwire: Tripwire,
240    ) -> Result<UpdateHandle, MatcherError> {
241        // check for existing handles
242        match schema.tables.get(tbl_name) {
243            Some(table) => {
244                if table.pk.is_empty() {
245                    return Err(MatcherError::MissingPrimaryKeys);
246                }
247            }
248            None => return Err(MatcherError::TableNotFound(tbl_name.to_string())),
249        };
250
251        let cancel = CancellationToken::new();
252        let (changes_tx, changes_rx) = mpsc::channel(20480);
253        let handle = UpdateHandle {
254            inner: Arc::new(InnerUpdateHandle {
255                id,
256                name: tbl_name.to_owned(),
257                cancel: cancel.clone(),
258                changes_tx,
259                counters: HandleMetrics {
260                    matched_count: counter!("corro.updates.changes.matched.count", "table" => tbl_name.to_owned()),
261                },
262            }),
263        };
264        spawn_counted(batch_candidates(id, cancel, evt_tx, changes_rx, tripwire));
265        Ok(handle)
266    }
267
268    pub async fn cleanup(self) {
269        self.inner.cancel.cancel();
270        info!(update_id = %self.inner.id, "Canceled update");
271    }
272}
273
274fn handle_candidates(
275    evt_tx: mpsc::Sender<NotifyEvent>,
276    candidates: MatchCandidates,
277) -> Result<(), MatcherError> {
278    if candidates.is_empty() {
279        return Ok(());
280    }
281
282    trace!(
283        "got some candidates for updates! {:?}",
284        candidates.keys().collect::<Vec<_>>()
285    );
286
287    for (_, pks) in candidates {
288        let pks = pks
289            .iter()
290            .map(|(pk, cl)| unpack_columns(pk).map(|x| (x, *cl)))
291            .collect::<Result<Vec<(Vec<SqliteValueRef>, i64)>, _>>()?;
292
293        for (pk, cl) in pks {
294            let mut change_type = ChangeType::Update;
295            if cl % 2 == 0 {
296                change_type = ChangeType::Delete
297            }
298            if let Err(e) = evt_tx.blocking_send(NotifyEvent::Notify(
299                change_type,
300                pk.iter().map(|x| x.to_owned()).collect::<Vec<_>>(),
301            )) {
302                debug!("could not send back row to matcher sub sender: {e}");
303                return Err(MatcherError::EventReceiverClosed);
304            }
305        }
306    }
307
308    Ok(())
309}
310
311async fn batch_candidates(
312    id: Uuid,
313    cancel: CancellationToken,
314    evt_tx: mpsc::Sender<NotifyEvent>,
315    mut changes_rx: mpsc::Receiver<MatchCandidates>,
316    mut tripwire: Tripwire,
317) {
318    const PROCESS_CHANGES_THRESHOLD: usize = 1000;
319    const PROCESS_BUFFER_DEADLINE: Duration = Duration::from_millis(600);
320    const MAX_CACHE_ENTRIES: usize = 2000;
321    const KEEP_CACHE_ENTRIES: usize = 1000;
322
323    // small cache to ensure we don't send an older update when changes to the same pk
324    // are made in quick succession. Changes aren't sent in order, and there's just one situation where this
325    // can be problematic, when we have a delete before an update but those
326    // get sent in reverse order. so the client might delete a key that's actually present in the db.
327    //
328    // (TODO: maybe we could send the cl to the client? or read from db?)
329    let mut cl_cache: IndexMap<(TableName, Vec<u8>), i64> = IndexMap::new();
330
331    info!(sub_id = %id, "Starting loop to receive candidates from updates");
332
333    let mut buf = MatchCandidates::new();
334    let mut buf_count = 0;
335
336    // max duration of aggregating candidates
337    let process_changes_deadline = tokio::time::sleep(PROCESS_BUFFER_DEADLINE);
338    tokio::pin!(process_changes_deadline);
339
340    let mut process = false;
341    loop {
342        tokio::select! {
343            biased;
344            _ = cancel.cancelled() => {
345                info!(sub_id = %id, "Acknowledged updates cancellation, breaking loop.");
346                break;
347            }
348            Some(candidates) = changes_rx.recv() => {
349                debug!(sub_id = %id, "updates got candidates: {candidates:?}");
350                for (table, pk_map) in  candidates {
351                    let buffed = buf.entry(table.clone()).or_default();
352
353                    for (pk, cl) in pk_map {
354                        let e = cl_cache.entry((table.clone(), pk.clone()));
355                        match e {
356                            Entry::Occupied(mut o) => {
357                                if *o.get() > cl {
358                                    continue;
359                                }
360                                o.insert(cl);
361                            }
362                            Entry::Vacant(v) => {
363                                v.insert(cl);
364                            }
365                        }
366
367                        buffed.insert(pk, cl);
368                        buf_count += 1;
369                    }
370                }
371
372                if cl_cache.len() > MAX_CACHE_ENTRIES {
373                    cl_cache = cl_cache.split_off(cl_cache.len() - KEEP_CACHE_ENTRIES);
374                }
375
376                if buf_count >= PROCESS_CHANGES_THRESHOLD {
377                   process = true
378                }
379            },
380            _ = process_changes_deadline.as_mut() => {
381                process_changes_deadline
382                    .as_mut()
383                    .reset(Instant::now() + PROCESS_BUFFER_DEADLINE);
384                if buf_count != 0 {
385                    process = true
386                }
387            },
388            _ = &mut tripwire => {
389                trace!(sub_id = %id, "tripped batch_candidates, returning");
390                return;
391            }
392            else => {
393                return;
394            }
395        }
396
397        if process {
398            let start = Instant::now();
399
400            if let Err(e) =
401                block_in_place(|| handle_candidates(evt_tx.clone(), std::mem::take(&mut buf)))
402            {
403                if !matches!(e, MatcherError::EventReceiverClosed) {
404                    error!(sub_id = %id, "could not handle change: {e}");
405                }
406                break;
407            }
408            let elapsed = start.elapsed();
409
410            histogram!("corro.updates.changes.processing.duration.seconds", "table" => id.to_string()).record(elapsed);
411
412            buf_count = 0;
413
414            // reset the deadline
415            process_changes_deadline
416                .as_mut()
417                .reset(Instant::now() + PROCESS_BUFFER_DEADLINE);
418        }
419    }
420
421    debug!(id = %id, "update loop is done");
422}
423
424pub fn match_changes<H>(manager: &impl Manager<H>, changes: &[Change], db_version: CrsqlDbVersion)
425where
426    H: Handle + Send + 'static,
427{
428    let trait_type = manager.trait_type();
429    trace!(
430        %db_version,
431        "trying to match changes to {trait_type}, len: {}",
432        changes.len()
433    );
434    if changes.is_empty() {
435        return;
436    }
437
438    let handles = manager.get_handles();
439    if handles.is_empty() {
440        return;
441    }
442
443    assert_sometimes!(true, "Corrosion matches changes for updates");
444    for (id, handle) in handles.iter() {
445        trace!(sub_id = %id, %db_version, "attempting to match changes to a subscription");
446        let mut candidates = MatchCandidates::new();
447        let mut match_count = 0;
448        for change in changes.iter().map(MatchableChange::from) {
449            if handle.filter_matchable_change(&mut candidates, change) {
450                match_count += 1;
451            }
452        }
453
454        // metrics...
455        for (table, pks) in candidates.iter() {
456            handle
457                .get_counter(table)
458                .matched_count
459                .increment(pks.len() as u64);
460        }
461
462        trace!(sub_id = %id, %db_version, "found {match_count} candidates");
463
464        if let Err(e) = handle.changes_tx().try_send(candidates) {
465            error!(sub_id = %id, "could not send change candidates to {trait_type} handler: {e}");
466            match e {
467                mpsc::error::TrySendError::Full(item) => {
468                    warn!("channel is full, falling back to async send");
469
470                    let changes_tx = handle.changes_tx();
471                    let id = *id;
472                    tokio::spawn(async move {
473                        if let Err(e) = changes_tx.send(item).await {
474                            error!(sub_id = %id, "could not send match_change candidates to handler: {e}");
475                        }
476                    });
477                }
478                mpsc::error::TrySendError::Closed(_) => {
479                    if let Some(handle) = manager.remove(id) {
480                        tokio::spawn(async move {
481                            handle.cleanup().await;
482                        });
483                    }
484                }
485            }
486        }
487    }
488}
489
490pub fn match_changes_from_db_version<H>(
491    manager: &impl Manager<H>,
492    conn: &Connection,
493    db_version: CrsqlDbVersion,
494    actor_id: ActorId,
495) -> rusqlite::Result<()>
496where
497    H: Handle + Send + 'static,
498{
499    let handles = manager.get_handles();
500    if handles.is_empty() {
501        return Ok(());
502    }
503
504    let trait_type = manager.trait_type();
505    let mut candidates = handles
506        .iter()
507        .map(|(id, handle)| (id, (MatchCandidates::new(), handle)))
508        .collect::<BTreeMap<_, _>>();
509
510    {
511        let mut prepped = conn.prepare_cached(
512            r#"
513        SELECT "table", pk, cid, cl
514            FROM crsql_changes
515            WHERE db_version = ?
516              AND site_id = ?
517            ORDER BY seq ASC
518        "#,
519        )?;
520
521        let rows = prepped.query_map((db_version, actor_id), |row| {
522            Ok((
523                row.get::<_, TableName>(0)?,
524                row.get::<_, Vec<u8>>(1)?,
525                row.get::<_, ColumnName>(2)?,
526                row.get::<_, i64>(3)?,
527            ))
528        })?;
529
530        for change_res in rows {
531            let (table, pk, column, cl) = change_res?;
532
533            for (_id, (candidates, handle)) in candidates.iter_mut() {
534                let change = MatchableChange {
535                    table: &table,
536                    pk: &pk,
537                    column: &column,
538                    cl,
539                };
540                handle.filter_matchable_change(candidates, change);
541            }
542        }
543    }
544
545    // metrics...
546    for (id, (candidates, handle)) in candidates {
547        let mut match_count = 0;
548        for (table, pks) in candidates.iter() {
549            let count = pks.len();
550            match_count += count;
551            handle
552                .get_counter(table)
553                .matched_count
554                .increment(pks.len() as u64);
555        }
556
557        trace!(sub_id = %id, %db_version, "found {match_count} candidates");
558
559        if let Err(e) = handle.changes_tx().try_send(candidates) {
560            error!(sub_id = %id, "could not send change candidates to {trait_type} handler: {e}");
561            match e {
562                mpsc::error::TrySendError::Full(item) => {
563                    warn!("channel is full, falling back to async send");
564                    let changes_tx = handle.changes_tx();
565                    let id = *id;
566                    tokio::spawn(async move {
567                        if let Err(e) = changes_tx.send(item).await {
568                            error!(sub_id = %id, "could not send change candidates to handler: {e}");
569                        }
570                    });
571                }
572                mpsc::error::TrySendError::Closed(_) => {
573                    if let Some(handle) = manager.remove(id) {
574                        tokio::spawn(async move {
575                            handle.cleanup().await;
576                        });
577                    }
578                }
579            }
580        }
581    }
582
583    Ok(())
584}