Skip to main content

klukai_types/
change.rs

1use std::iter::Peekable;
2
3pub use crate::api::SqliteValue;
4use crate::api::{ColumnName, TableName};
5use antithesis_sdk::assert_always;
6use rusqlite::{Connection, Row};
7use serde::{Deserialize, Serialize};
8use serde_json::json;
9use speedy::{Readable, Writable};
10use tracing::{debug, trace, warn};
11
12use crate::{
13    agent::{Agent, BookedVersions, ChangeError, VersionsSnapshot},
14    base::{CrsqlDbVersion, CrsqlSeq, CrsqlSeqRange},
15    broadcast::{ChangesetPerTable, Timestamp},
16};
17
18#[derive(Debug, Default, Clone, Serialize, Deserialize, Readable, Writable, PartialEq)]
19pub struct Change {
20    pub table: TableName,
21    pub pk: Vec<u8>,
22    pub cid: ColumnName,
23    pub val: SqliteValue,
24    pub col_version: i64,
25    pub db_version: CrsqlDbVersion,
26    pub seq: CrsqlSeq,
27    pub site_id: [u8; 16],
28    pub cl: i64,
29}
30
31impl Change {
32    // this is an ESTIMATE, it should give a rough idea of how many bytes will
33    // be required on the wire
34    pub fn estimated_byte_size(&self) -> usize {
35        self.table.len() + self.pk.len() + self.cid.len() + self.val.estimated_byte_size() +
36        // db_version
37        8 +
38        self.estimated_column_byte_size() +
39        // site_id
40        16
41    }
42
43    pub fn estimated_column_byte_size(&self) -> usize {
44        self.cid.len() + self.val.estimated_byte_size() +
45        // col_version
46        8 +
47        // seq
48        8 +
49        // cl
50        8
51    }
52}
53
54pub fn row_to_change(row: &Row) -> Result<Change, rusqlite::Error> {
55    Ok(Change {
56        table: row.get(0)?,
57        pk: row.get(1)?,
58        cid: row.get(2)?,
59        val: row.get(3)?,
60        col_version: row.get(4)?,
61        db_version: row.get(5)?,
62        seq: row.get(6)?,
63        site_id: row.get(7)?,
64        cl: row.get(8)?,
65    })
66}
67
68pub struct ChunkedChanges<I: Iterator> {
69    iter: Peekable<I>,
70    changes: ChangesetPerTable,
71    last_pushed_seq: CrsqlSeq,
72    last_start_seq: CrsqlSeq,
73    last_seq: CrsqlSeq,
74    max_buf_size: usize,
75    buffered_size: usize,
76    done: bool,
77}
78
79impl<I> ChunkedChanges<I>
80where
81    I: Iterator,
82{
83    pub fn new(iter: I, start_seq: CrsqlSeq, last_seq: CrsqlSeq, max_buf_size: usize) -> Self {
84        Self {
85            iter: iter.peekable(),
86            changes: Default::default(),
87            last_pushed_seq: CrsqlSeq(0),
88            last_start_seq: start_seq,
89            last_seq,
90            max_buf_size,
91            buffered_size: 0,
92            done: false,
93        }
94    }
95
96    pub fn max_buf_size(&self) -> usize {
97        self.max_buf_size
98    }
99
100    pub fn set_max_buf_size(&mut self, size: usize) {
101        self.max_buf_size = size;
102    }
103}
104
105impl<I> Iterator for ChunkedChanges<I>
106where
107    I: Iterator<Item = rusqlite::Result<Change>>,
108{
109    type Item = Result<(ChangesetPerTable, CrsqlSeqRange), rusqlite::Error>;
110
111    fn next(&mut self) -> Option<Self::Item> {
112        // previously marked as done because the Rows iterator returned None
113        if self.done {
114            return None;
115        }
116
117        let details = json!({});
118        assert_always!(
119            self.changes.is_empty(),
120            "iterator for ChunkedChanges still has changes when next() is called",
121            &details
122        );
123
124        // reset the buffered size
125        self.buffered_size = 0;
126
127        loop {
128            trace!("chunking through the rows iterator");
129            match self.iter.next() {
130                Some(Ok(change)) => {
131                    trace!("got change: {change:?}");
132
133                    self.last_pushed_seq = change.seq;
134
135                    let size = self.changes.insert(change);
136                    self.buffered_size += size;
137
138                    if self.last_pushed_seq == self.last_seq {
139                        // this was the last seq! break early
140                        break;
141                    }
142
143                    if self.buffered_size >= self.max_buf_size {
144                        // chunking it up
145                        let start_seq = self.last_start_seq;
146
147                        if self.iter.peek().is_none() {
148                            // no more rows, break early
149                            break;
150                        }
151
152                        // prepare for next round! we're not done...
153                        self.last_start_seq = self.last_pushed_seq + 1;
154
155                        return Some(Ok((
156                            self.changes.drain(),
157                            CrsqlSeqRange::new(start_seq, self.last_pushed_seq),
158                        )));
159                    }
160                }
161                None => {
162                    // probably not going to happen since we peek at the next and end early
163                    // break out of the loop, don't return, there might be buffered changes
164                    trace!("no more changes to iterate on");
165                    break;
166                }
167                Some(Err(e)) => return Some(Err(e)),
168            }
169        }
170
171        self.done = true;
172
173        // return buffered changes
174        Some(Ok((
175            self.changes.clone(), // no need to drain here like before
176            CrsqlSeqRange::new(self.last_start_seq, self.last_seq), // even if empty, this is all we have still applied
177        )))
178    }
179}
180
181pub const MAX_CHANGES_BYTE_SIZE: usize = 8 * 1024;
182
183pub struct InsertChangesInfo {
184    pub db_version: CrsqlDbVersion,
185    pub last_seq: CrsqlSeq,
186    pub ts: Timestamp,
187    pub snap: VersionsSnapshot,
188}
189
190pub fn insert_local_changes(
191    agent: &Agent,
192    tx: &Connection,
193    book_writer: &mut tokio::sync::RwLockWriteGuard<'_, BookedVersions>,
194) -> Result<Option<InsertChangesInfo>, ChangeError> {
195    let actor_id = agent.actor_id();
196
197    let db_version: CrsqlDbVersion = tx
198        .prepare_cached("SELECT crsql_peek_next_db_version()")
199        .map_err(|source| ChangeError::Rusqlite {
200            source,
201            actor_id: Some(actor_id),
202            version: None,
203        })?
204        .query_row((), |row| row.get(0))
205        .map_err(|source| ChangeError::Rusqlite {
206            source,
207            actor_id: Some(actor_id),
208            version: None,
209        })?;
210
211    let version_info: (Option<CrsqlSeq>, Option<Timestamp>) = tx
212        .prepare_cached(
213            "SELECT MAX(seq), MAX(ts) FROM crsql_changes WHERE site_id = ? AND db_version = ?;",
214        )
215        .map_err(|source| ChangeError::Rusqlite {
216            source,
217            actor_id: Some(actor_id),
218            version: None,
219        })?
220        .query_row((agent.actor_id(), db_version), |row| {
221            Ok((row.get(0)?, row.get(1)?))
222        })
223        .map_err(|source| ChangeError::Rusqlite {
224            source,
225            actor_id: Some(actor_id),
226            version: None,
227        })?;
228
229    match version_info {
230        (None, None) => Ok(None),
231        (None, Some(ts)) => {
232            warn!("found db_version {db_version} without seq, last ts: {ts:?})");
233            Ok(None)
234        }
235        (Some(last_seq), ts) => {
236            let ts = ts.unwrap_or_else(|| {
237                warn!("found db_version {db_version} without seq, last ts: {ts:?}");
238                Timestamp::from(agent.clock().new_timestamp())
239            });
240
241            debug!("found db_version {db_version} (last seq: {last_seq}, last ts: {ts})");
242
243            let db_versions = db_version..=db_version;
244
245            let mut snap = book_writer.snapshot();
246            snap.insert_db(tx, [db_versions].into())
247                .map_err(|source| ChangeError::Rusqlite {
248                    source,
249                    actor_id: Some(actor_id),
250                    version: Some(db_version),
251                })?;
252
253            Ok(Some(InsertChangesInfo {
254                db_version,
255                last_seq,
256                ts,
257                snap,
258            }))
259        }
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266    use crate::dbsr;
267
268    #[test]
269    fn test_change_chunker() {
270        // empty interator
271        let mut chunker = ChunkedChanges::new(vec![].into_iter(), CrsqlSeq(0), CrsqlSeq(100), 50);
272
273        assert_eq!(
274            chunker.next(),
275            Some(Ok((ChangesetPerTable::default(), dbsr!(0, 100))))
276        );
277        assert_eq!(chunker.next(), None);
278
279        let changes: Vec<Change> = (0..100)
280            .map(|seq| Change {
281                seq: CrsqlSeq(seq),
282                ..Default::default()
283            })
284            .collect();
285
286        let (changeset, size) =
287            mapped_changeset_from_changes(vec![changes[0].clone(), changes[1].clone()]);
288        // 2 iterations
289        let mut chunker = ChunkedChanges::new(
290            vec![
291                Ok(changes[0].clone()),
292                Ok(changes[1].clone()),
293                Ok(changes[2].clone()),
294            ]
295            .into_iter(),
296            CrsqlSeq(0),
297            CrsqlSeq(100),
298            size,
299        );
300
301        assert_eq!(chunker.next(), Some(Ok((changeset, dbsr!(0, 1)))));
302
303        let (changeset, _) = mapped_changeset_from_changes(vec![changes[2].clone()]);
304        assert_eq!(chunker.next(), Some(Ok((changeset, dbsr!(2, 100)))));
305        assert_eq!(chunker.next(), None);
306
307        let (changeset, size) = mapped_changeset_from_changes(vec![changes[0].clone()]);
308        let mut chunker = ChunkedChanges::new(
309            vec![Ok(changes[0].clone()), Ok(changes[1].clone())].into_iter(),
310            CrsqlSeq(0),
311            CrsqlSeq(0),
312            size,
313        );
314
315        assert_eq!(chunker.next(), Some(Ok((changeset, dbsr!(0, 0)))));
316        assert_eq!(chunker.next(), None);
317
318        let (changeset, size) =
319            mapped_changeset_from_changes(vec![changes[0].clone(), changes[2].clone()]);
320        // gaps
321        let mut chunker = ChunkedChanges::new(
322            vec![Ok(changes[0].clone()), Ok(changes[2].clone())].into_iter(),
323            CrsqlSeq(0),
324            CrsqlSeq(100),
325            size,
326        );
327
328        assert_eq!(chunker.next(), Some(Ok((changeset, dbsr!(0, 100)))));
329
330        assert_eq!(chunker.next(), None);
331
332        // gaps
333        let (changeset, _) = mapped_changeset_from_changes(vec![
334            changes[2].clone(),
335            changes[4].clone(),
336            changes[7].clone(),
337            changes[8].clone(),
338        ]);
339        let mut chunker = ChunkedChanges::new(
340            vec![
341                Ok(changes[2].clone()),
342                Ok(changes[4].clone()),
343                Ok(changes[7].clone()),
344                Ok(changes[8].clone()),
345            ]
346            .into_iter(),
347            CrsqlSeq(0),
348            CrsqlSeq(100),
349            100000, // just send them all!
350        );
351
352        assert_eq!(chunker.next(), Some(Ok((changeset, dbsr!(0, 100)))));
353
354        assert_eq!(chunker.next(), None);
355
356        // gaps
357        let (changeset, size) =
358            mapped_changeset_from_changes(vec![changes[2].clone(), changes[4].clone()]);
359        let mut chunker = ChunkedChanges::new(
360            vec![
361                Ok(changes[2].clone()),
362                Ok(changes[4].clone()),
363                Ok(changes[7].clone()),
364                Ok(changes[8].clone()),
365            ]
366            .into_iter(),
367            CrsqlSeq(0),
368            CrsqlSeq(10),
369            size,
370        );
371
372        assert_eq!(chunker.next(), Some(Ok((changeset, dbsr!(0, 4)))));
373
374        let (changeset, _) =
375            mapped_changeset_from_changes(vec![changes[7].clone(), changes[8].clone()]);
376        assert_eq!(chunker.next(), Some(Ok((changeset, dbsr!(5, 10)))));
377
378        assert_eq!(chunker.next(), None);
379    }
380
381    fn mapped_changeset_from_changes(changes: Vec<Change>) -> (ChangesetPerTable, usize) {
382        let mut changeset = ChangesetPerTable::default();
383        let mut size = 0;
384        for change in changes {
385            size += changeset.insert(change);
386        }
387        (changeset, size)
388    }
389}