klukai_types/
broadcast.rs

1use std::{
2    cmp, fmt, io,
3    num::NonZeroU32,
4    num::ParseIntError,
5    ops::{Deref, RangeInclusive},
6    time::Duration,
7};
8
9use crate::api::{ColumnName, SqliteValue};
10use antithesis_sdk::assert_sometimes;
11use bytes::{Bytes, BytesMut};
12use foca::{Identity, Member, Notification, OwnedNotification, Runtime, Timer};
13use itertools::Itertools;
14use metrics::counter;
15use rusqlite::{
16    ToSql,
17    types::{FromSql, FromSqlError},
18};
19use serde::{Deserialize, Serialize};
20use speedy::{Context, Readable, Reader, Writable, Writer};
21use time::OffsetDateTime;
22use tokio::{
23    sync::{mpsc, oneshot},
24    task::block_in_place,
25};
26use tracing::{debug, error, trace};
27use uhlc::NTP64;
28
29use crate::{
30    actor::{Actor, ActorId, ClusterId},
31    agent::Agent,
32    base::{CrsqlDbVersion, CrsqlSeq},
33    change::{Change, ChunkedChanges, MAX_CHANGES_BYTE_SIZE, row_to_change},
34    channel::CorroSender,
35    sqlite::SqlitePoolError,
36    sync::SyncTraceContextV1,
37    updates::match_changes,
38};
39
40#[derive(Debug, Clone, Readable, Writable)]
41pub enum UniPayload {
42    V1 {
43        data: UniPayloadV1,
44        #[speedy(default_on_eof)]
45        cluster_id: ClusterId,
46    },
47}
48
49#[derive(Debug, Clone, Readable, Writable)]
50pub enum UniPayloadV1 {
51    Broadcast(BroadcastV1),
52}
53
54#[derive(Debug, Clone, Readable, Writable)]
55pub enum BiPayload {
56    V1 {
57        data: BiPayloadV1,
58        #[speedy(default_on_eof)]
59        cluster_id: ClusterId,
60    },
61}
62
63#[derive(Debug, Clone, Readable, Writable)]
64pub enum BiPayloadV1 {
65    SyncStart {
66        actor_id: ActorId,
67        #[speedy(default_on_eof)]
68        trace_ctx: SyncTraceContextV1,
69    },
70}
71
72#[derive(Debug)]
73pub enum FocaInput {
74    Announce(Actor),
75    Data(Bytes),
76    ClusterSize(NonZeroU32),
77    ApplyMany(Vec<Member<Actor>>),
78    Cmd(FocaCmd),
79}
80
81#[derive(Debug)]
82pub enum FocaCmd {
83    Rejoin(oneshot::Sender<Result<(), foca::Error>>),
84    MembershipStates(mpsc::Sender<foca::Member<Actor>>),
85    ChangeIdentity(Actor, oneshot::Sender<Result<(), foca::Error>>),
86}
87
88#[derive(Debug, Clone, Readable, Writable)]
89pub enum AuthzV1 {
90    Token(String),
91}
92
93#[derive(Clone, Debug, Readable, Writable)]
94pub enum BroadcastV1 {
95    Change(ChangeV1),
96}
97
98#[derive(Debug, Clone, PartialEq, Readable, Writable)]
99pub struct ColumnChange {
100    pub cid: ColumnName,
101    pub val: SqliteValue,
102    pub col_version: i64,
103    pub seq: CrsqlSeq,
104    pub cl: i64,
105}
106
107#[derive(Debug, Clone, Copy, strum::IntoStaticStr)]
108#[strum(serialize_all = "snake_case")]
109pub enum ChangeSource {
110    Broadcast,
111    Sync,
112}
113
114#[derive(Debug, Clone, PartialEq, Readable, Writable)]
115pub struct ChangeV1 {
116    pub actor_id: ActorId,
117    pub changeset: Changeset,
118}
119
120impl Deref for ChangeV1 {
121    type Target = Changeset;
122
123    fn deref(&self) -> &Self::Target {
124        &self.changeset
125    }
126}
127
128#[derive(Debug, Clone, PartialEq, Readable, Writable)]
129pub enum Changeset {
130    Empty {
131        versions: RangeInclusive<CrsqlDbVersion>,
132        #[speedy(default_on_eof)]
133        ts: Option<Timestamp>,
134    },
135    Full {
136        version: CrsqlDbVersion,
137        changes: Vec<Change>,
138        // cr-sqlite sequences contained in this changeset
139        seqs: RangeInclusive<CrsqlSeq>,
140        // last cr-sqlite sequence for the complete changeset
141        last_seq: CrsqlSeq,
142        ts: Timestamp,
143    },
144    EmptySet {
145        versions: Vec<RangeInclusive<CrsqlDbVersion>>,
146        ts: Timestamp,
147    },
148}
149
150impl From<ChangesetParts> for Changeset {
151    fn from(value: ChangesetParts) -> Self {
152        Changeset::Full {
153            version: value.version,
154            changes: value.changes,
155            seqs: value.seqs,
156            last_seq: value.last_seq,
157            ts: value.ts,
158        }
159    }
160}
161
162pub struct ChangesetParts {
163    pub version: CrsqlDbVersion,
164    pub changes: Vec<Change>,
165    pub seqs: RangeInclusive<CrsqlSeq>,
166    pub last_seq: CrsqlSeq,
167    pub ts: Timestamp,
168}
169
170impl Changeset {
171    pub fn versions(&self) -> RangeInclusive<CrsqlDbVersion> {
172        match self {
173            Changeset::Empty { versions, .. } => versions.clone(),
174            // todo: this returns dummy version because empty set has an array of versions.
175            // probably shouldn't be doing this
176            Changeset::EmptySet { .. } => CrsqlDbVersion(0)..=CrsqlDbVersion(0),
177            Changeset::Full { version, .. } => *version..=*version,
178        }
179    }
180
181    // determine the estimated resource cost of processing a change
182    pub fn processing_cost(&self) -> usize {
183        match self {
184            Changeset::Empty { versions, .. } => {
185                cmp::min((versions.end().0 - versions.start().0) as usize + 1, 20)
186            }
187            Changeset::EmptySet { versions, .. } => versions
188                .iter()
189                .map(|versions| cmp::min((versions.end().0 - versions.start().0) as usize + 1, 20))
190                .sum::<usize>(),
191            Changeset::Full { changes, .. } => changes.len(),
192        }
193    }
194
195    pub fn max_db_version(&self) -> Option<CrsqlDbVersion> {
196        self.changes().iter().map(|c| c.db_version).max()
197    }
198
199    pub fn seqs(&self) -> Option<&RangeInclusive<CrsqlSeq>> {
200        match self {
201            Changeset::Empty { .. } => None,
202            Changeset::EmptySet { .. } => None,
203            Changeset::Full { seqs, .. } => Some(seqs),
204        }
205    }
206
207    pub fn last_seq(&self) -> Option<CrsqlSeq> {
208        match self {
209            Changeset::Empty { .. } => None,
210            Changeset::EmptySet { .. } => None,
211            Changeset::Full { last_seq, .. } => Some(*last_seq),
212        }
213    }
214
215    pub fn is_complete(&self) -> bool {
216        match self {
217            Changeset::Empty { .. } => true,
218            Changeset::EmptySet { .. } => true,
219            Changeset::Full { seqs, last_seq, .. } => {
220                *seqs.start() == CrsqlSeq(0) && seqs.end() == last_seq
221            }
222        }
223    }
224
225    pub fn len(&self) -> usize {
226        match self {
227            Changeset::Empty { .. } => 0, //(versions.end().0 - versions.start().0 + 1) as usize,
228            Changeset::EmptySet { versions, .. } => versions.len(),
229            Changeset::Full { changes, .. } => changes.len(),
230        }
231    }
232
233    pub fn is_empty(&self) -> bool {
234        match self {
235            Changeset::Empty { .. } => true,
236            Changeset::EmptySet { .. } => true,
237            Changeset::Full { changes, .. } => changes.is_empty(),
238        }
239    }
240
241    pub fn is_empty_set(&self) -> bool {
242        match self {
243            Changeset::Empty { .. } => false,
244            Changeset::EmptySet { .. } => true,
245            Changeset::Full { .. } => false,
246        }
247    }
248
249    pub fn ts(&self) -> Option<Timestamp> {
250        match self {
251            Changeset::Empty { ts, .. } => *ts,
252            Changeset::EmptySet { ts, .. } => Some(*ts),
253            Changeset::Full { ts, .. } => Some(*ts),
254        }
255    }
256
257    pub fn changes(&self) -> &[Change] {
258        match self {
259            Changeset::Empty { .. } => &[],
260            Changeset::EmptySet { .. } => &[],
261            Changeset::Full { changes, .. } => changes,
262        }
263    }
264
265    pub fn into_parts(self) -> Option<ChangesetParts> {
266        match self {
267            Changeset::Empty { .. } => None,
268            Changeset::EmptySet { .. } => None,
269            Changeset::Full {
270                version,
271                changes,
272                seqs,
273                last_seq,
274                ts,
275            } => Some(ChangesetParts {
276                version,
277                changes,
278                seqs,
279                last_seq,
280                ts,
281            }),
282        }
283    }
284}
285
286#[derive(Debug, thiserror::Error)]
287pub enum TimestampParseError {
288    #[error("could not parse timestamp: {0:?}")]
289    Parse(ParseIntError),
290}
291
292#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, Eq, PartialOrd, Ord)]
293#[serde(transparent)]
294pub struct Timestamp(pub NTP64);
295
296impl Timestamp {
297    pub fn to_time(&self) -> OffsetDateTime {
298        OffsetDateTime::from_unix_timestamp(self.0.as_secs() as i64).unwrap()
299            + time::Duration::nanoseconds(self.0.subsec_nanos() as i64)
300    }
301
302    pub fn to_ntp64(&self) -> NTP64 {
303        self.0
304    }
305
306    pub fn zero() -> Self {
307        Timestamp::from(0u64)
308    }
309
310    pub fn is_zero(&self) -> bool {
311        self.0.0 == 0
312    }
313}
314
315// formatting to humantime and then parsing again incurs oddness, so lets compare secs and subsec_nanos
316impl PartialEq for Timestamp {
317    fn eq(&self, other: &Self) -> bool {
318        self.0.as_secs() == other.0.as_secs() && self.0.subsec_nanos() == other.0.subsec_nanos()
319    }
320}
321
322impl std::hash::Hash for Timestamp {
323    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
324        self.0.as_secs().hash(state);
325        self.0.subsec_nanos().hash(state);
326    }
327}
328
329impl Deref for Timestamp {
330    type Target = NTP64;
331
332    fn deref(&self) -> &Self::Target {
333        &self.0
334    }
335}
336
337impl fmt::Display for Timestamp {
338    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
339        self.0.fmt(f)
340    }
341}
342
343impl From<uhlc::Timestamp> for Timestamp {
344    fn from(ts: uhlc::Timestamp) -> Self {
345        Self(*ts.get_time())
346    }
347}
348
349impl From<NTP64> for Timestamp {
350    fn from(ntp64: NTP64) -> Self {
351        Self(ntp64)
352    }
353}
354
355impl From<u64> for Timestamp {
356    fn from(value: u64) -> Self {
357        Self(NTP64(value))
358    }
359}
360
361impl FromSql for Timestamp {
362    fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult<Self> {
363        match value {
364            rusqlite::types::ValueRef::Text(b) => match std::str::from_utf8(b) {
365                Ok(s) => match s.parse::<u64>() {
366                    Ok(ntp) => Ok(Timestamp(NTP64(ntp))),
367                    Err(e) => Err(FromSqlError::Other(Box::new(TimestampParseError::Parse(e)))),
368                },
369                Err(e) => Err(FromSqlError::Other(Box::new(e))),
370            },
371            _ => Err(FromSqlError::InvalidType),
372        }
373    }
374}
375
376impl ToSql for Timestamp {
377    fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
378        Ok(rusqlite::types::ToSqlOutput::Owned(
379            rusqlite::types::Value::Text(self.0.as_u64().to_string()),
380        ))
381    }
382}
383
384impl<'a, C> Readable<'a, C> for Timestamp
385where
386    C: Context,
387{
388    #[inline]
389    fn read_from<R: Reader<'a, C>>(reader: &mut R) -> Result<Self, C::Error> {
390        Ok(Timestamp(NTP64(u64::read_from(reader)?)))
391    }
392
393    #[inline]
394    fn minimum_bytes_needed() -> usize {
395        std::mem::size_of::<u64>()
396    }
397}
398
399impl<C> Writable<C> for Timestamp
400where
401    C: Context,
402{
403    #[inline]
404    fn write_to<T: ?Sized + Writer<C>>(&self, writer: &mut T) -> Result<(), C::Error> {
405        self.0.0.write_to(writer)
406    }
407
408    #[inline]
409    fn bytes_needed(&self) -> Result<usize, C::Error> {
410        <u64 as speedy::Writable<C>>::bytes_needed(&self.0.0)
411    }
412}
413
414#[derive(Debug, thiserror::Error)]
415pub enum BroadcastEncodeError {
416    #[error(transparent)]
417    Encode(#[from] speedy::Error),
418    #[error(transparent)]
419    Io(#[from] io::Error),
420}
421
422#[derive(Debug, thiserror::Error)]
423pub enum BroadcastDecodeError {
424    #[error(transparent)]
425    Decode(#[from] speedy::Error),
426    #[error("corrupted message, crc mismatch (got: {0}, expected {1})")]
427    Corrupted(u32, u32),
428    #[error(transparent)]
429    Io(#[from] io::Error),
430    #[error("insufficient length received to decode message: {0}")]
431    InsufficientLength(usize),
432}
433
434#[derive(Debug)]
435pub enum BroadcastInput {
436    Rebroadcast(BroadcastV1),
437    AddBroadcast(BroadcastV1),
438}
439
440pub struct DispatchRuntime<T> {
441    pub to_send: CorroSender<(T, Bytes)>,
442    pub to_schedule: CorroSender<(Duration, Timer<T>)>,
443    pub notifications: CorroSender<OwnedNotification<T>>,
444    pub active: bool,
445    pub buf: BytesMut,
446}
447
448impl<T: Identity> Runtime<T> for DispatchRuntime<T> {
449    fn notify(&mut self, notification: Notification<'_, T>) {
450        match &notification {
451            Notification::Active => {
452                self.active = true;
453            }
454            Notification::Idle | Notification::Defunct => {
455                self.active = false;
456            }
457            _ => {}
458        };
459
460        // Convert the borrowed notification to an owned one for sending through the channel
461        let owned_notification = notification.to_owned();
462
463        if let Err(e) = self.notifications.try_send(owned_notification) {
464            counter!("corro.channel.error", "type" => "full", "name" => "dispatch.notifications")
465                .increment(1);
466            error!("error dispatching notification: {e}");
467        }
468    }
469
470    fn send_to(&mut self, to: T, data: &[u8]) {
471        trace!("cluster send_to {to:?}");
472        self.buf.extend_from_slice(data);
473
474        if let Err(e) = self.to_send.try_send((to, self.buf.split().freeze())) {
475            counter!("corro.channel.error", "type" => "full", "name" => "dispatch.to_send")
476                .increment(1);
477            error!("error dispatching broadcast packet: {e}");
478        }
479    }
480
481    fn submit_after(&mut self, event: Timer<T>, after: Duration) {
482        if let Err(e) = self.to_schedule.try_send((after, event)) {
483            counter!("corro.channel.error", "type" => "full", "name" => "dispatch.to_schedule")
484                .increment(1);
485            error!("error dispatching scheduled event: {e}");
486        }
487    }
488}
489
490impl<T> DispatchRuntime<T> {
491    pub fn new(
492        to_send: CorroSender<(T, Bytes)>,
493        to_schedule: CorroSender<(Duration, Timer<T>)>,
494        notifications: CorroSender<OwnedNotification<T>>,
495    ) -> Self {
496        Self {
497            to_send,
498            to_schedule,
499            notifications,
500            active: false,
501            buf: BytesMut::new(),
502        }
503    }
504}
505
506#[derive(Debug, thiserror::Error)]
507pub enum BroadcastError {
508    #[error(transparent)]
509    Pool(#[from] SqlitePoolError),
510    #[error(transparent)]
511    Sqlite(#[from] rusqlite::Error),
512}
513
514pub async fn broadcast_changes(
515    agent: Agent,
516    db_version: CrsqlDbVersion,
517    last_seq: CrsqlSeq,
518    ts: Timestamp,
519) -> Result<(), BroadcastError> {
520    let actor_id = agent.actor_id();
521    let conn = agent.pool().read().await?;
522    trace!("got conn for broadcast");
523
524    block_in_place(|| {
525        // TODO: make this more generic so both sync and local changes can use it.
526        let mut prepped = conn.prepare_cached(
527            r#"
528                SELECT "table", pk, cid, val, col_version, db_version, seq, site_id, cl
529                    FROM crsql_changes
530                    WHERE db_version = ?
531                    AND site_id = crsql_site_id()
532                    ORDER BY seq ASC
533            "#,
534        )?;
535        let rows = prepped.query_map([db_version], row_to_change)?;
536        let chunked = ChunkedChanges::new(rows, CrsqlSeq(0), last_seq, MAX_CHANGES_BYTE_SIZE);
537        for changes_seqs in chunked {
538            match changes_seqs {
539                Ok((changes, seqs)) => {
540                    for (table_name, count) in changes.iter().counts_by(|change| &change.table) {
541                        counter!("corro.changes.committed", "table" => table_name.to_string(), "source" => "local").increment(count as u64);
542                    }
543
544                    trace!("broadcasting changes: {changes:?} for seq: {seqs:?}");
545
546                    debug!("match_changes db_version: {db_version}");
547                    match_changes(agent.subs_manager(), &changes, db_version);
548                    match_changes(agent.updates_manager(), &changes, db_version);
549
550                    let tx_bcast = agent.tx_bcast().clone();
551                    assert_sometimes!(true, "Corrosion broadcasts changes");
552                    tokio::spawn(async move {
553                        if let Err(e) = tx_bcast
554                            .send(BroadcastInput::AddBroadcast(BroadcastV1::Change(
555                                ChangeV1 {
556                                    actor_id,
557                                    changeset: Changeset::Full {
558                                        version: db_version,
559                                        changes,
560                                        seqs,
561                                        last_seq,
562                                        ts,
563                                    },
564                                },
565                            )))
566                            .await
567                        {
568                            error!("could not send change message for broadcast: {e}");
569                        }
570                    });
571                }
572                Err(e) => {
573                    error!(
574                        "could not process crsql change (db_version: {db_version}) for broadcast: {e}"
575                    );
576                    break;
577                }
578            }
579        }
580        Ok::<_, rusqlite::Error>(())
581    })?;
582
583    Ok(())
584}