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::broadcast::Changeset;
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 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 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
147pub 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 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 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 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 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: &Changeset, 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.matchable_changes() {
449 if handle.filter_matchable_change(&mut candidates, change) {
450 match_count += 1;
451 }
452 }
453
454 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 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}