klukai_types/
change.rs

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