klukai_types/
sync.rs

1use std::{cmp, collections::HashMap, io, ops::RangeInclusive};
2
3use bytes::BytesMut;
4use opentelemetry::propagation::{Extractor, Injector};
5use rangemap::RangeInclusiveSet;
6use serde::{Deserialize, Serialize};
7use speedy::{Readable, Writable};
8use tokio_util::codec::{Decoder, LengthDelimitedCodec};
9use tracing::warn;
10
11use crate::{
12    actor::ActorId,
13    agent::{Booked, Bookie},
14    base::{CrsqlDbVersion, CrsqlSeq},
15    broadcast::{ChangeV1, Timestamp},
16};
17
18#[derive(Debug, Clone, PartialEq, Readable, Writable)]
19pub enum SyncMessage {
20    V1(SyncMessageV1),
21}
22
23#[derive(Debug, Clone, PartialEq, Readable, Writable)]
24pub enum SyncMessageV1 {
25    State(SyncStateV1),
26    Changeset(ChangeV1),
27    Clock(Timestamp),
28    Rejection(SyncRejectionV1),
29    Request(SyncRequestV1),
30}
31
32#[derive(Debug, Default, Clone, PartialEq, Readable, Writable)]
33pub struct SyncTraceContextV1 {
34    pub traceparent: Option<String>,
35    pub tracestate: Option<String>,
36}
37
38impl Injector for SyncTraceContextV1 {
39    fn set(&mut self, key: &str, value: String) {
40        match key {
41            "traceparent" if !value.is_empty() => self.traceparent = Some(value),
42            "tracestate" if !value.is_empty() => self.tracestate = Some(value),
43            _ => {}
44        }
45    }
46}
47
48impl Extractor for SyncTraceContextV1 {
49    fn get(&self, key: &str) -> Option<&str> {
50        match key {
51            "traceparent" => self.traceparent.as_deref(),
52            "tracestate" => self.tracestate.as_deref(),
53            _ => None,
54        }
55    }
56
57    fn keys(&self) -> Vec<&str> {
58        let mut v = Vec::with_capacity(2);
59        if self.traceparent.is_some() {
60            v.push("traceparent");
61        }
62        if self.tracestate.is_some() {
63            v.push("tracestate");
64        }
65        v
66    }
67}
68
69pub type SyncRequestV1 = Vec<(ActorId, Vec<SyncNeedV1>)>;
70
71#[derive(Debug, thiserror::Error, Clone, PartialEq, Readable, Writable)]
72pub enum SyncRejectionV1 {
73    #[error("max concurrency reached")]
74    MaxConcurrencyReached,
75    #[error("different cluster")]
76    DifferentCluster,
77}
78
79#[derive(Debug, Default, Clone, PartialEq, Readable, Writable, Serialize, Deserialize)]
80pub struct SyncStateV1 {
81    pub actor_id: ActorId,
82    pub heads: HashMap<ActorId, CrsqlDbVersion>,
83    pub need: HashMap<ActorId, Vec<RangeInclusive<CrsqlDbVersion>>>,
84    pub partial_need: HashMap<ActorId, HashMap<CrsqlDbVersion, Vec<RangeInclusive<CrsqlSeq>>>>,
85    #[speedy(default_on_eof)]
86    pub last_cleared_ts: Option<Timestamp>,
87}
88
89impl SyncStateV1 {
90    pub fn need_len(&self) -> u64 {
91        self.need
92            .values()
93            .flat_map(|v| v.iter().map(|range| (range.end().0 - range.start().0) + 1))
94            .sum::<u64>()
95            + (
96                self.partial_need
97                    .values()
98                    .flat_map(|partials| {
99                        partials.values().flat_map(|ranges| {
100                            ranges
101                                .iter()
102                                .map(|range| (range.end().0 - range.start().0) + 1)
103                        })
104                    })
105                    .sum::<u64>()
106                    / 50
107                // this is how many chunks we're looking at, kind of random...
108            )
109    }
110
111    pub fn need_len_for_actor(&self, actor_id: &ActorId) -> u64 {
112        self.need
113            .get(actor_id)
114            .map(|v| {
115                v.iter()
116                    .map(|range| (range.end().0 - range.start().0) + 1)
117                    .sum()
118            })
119            .unwrap_or(0)
120            + self
121                .partial_need
122                .get(actor_id)
123                .map(|partials| partials.len() as u64)
124                .unwrap_or(0)
125    }
126
127    pub fn compute_available_needs(
128        &self,
129        other: &SyncStateV1,
130    ) -> HashMap<ActorId, Vec<SyncNeedV1>> {
131        let mut needs: HashMap<ActorId, Vec<SyncNeedV1>> = HashMap::new();
132
133        for (actor_id, head) in other.heads.iter() {
134            if *actor_id == self.actor_id {
135                continue;
136            }
137            if *head == CrsqlDbVersion(0) {
138                warn!(actor_id = %other.actor_id, "sent a 0 head version for actor id {}", actor_id);
139                continue;
140            }
141            let other_haves = {
142                let mut haves =
143                    RangeInclusiveSet::from_iter([(CrsqlDbVersion(1)..=*head)].into_iter());
144
145                // remove needs
146                if let Some(other_need) = other.need.get(actor_id) {
147                    for need in other_need.iter() {
148                        // create gaps
149                        haves.remove(need.clone());
150                    }
151                }
152
153                // remove partials
154                if let Some(other_partials) = other.partial_need.get(actor_id) {
155                    for (v, _) in other_partials.iter() {
156                        haves.remove(*v..=*v);
157                    }
158                }
159
160                // we are left with all the versions they fully have!
161                haves
162            };
163
164            if let Some(our_need) = self.need.get(actor_id) {
165                for range in our_need.iter() {
166                    for overlap in other_haves.overlapping(range) {
167                        let start = cmp::max(range.start(), overlap.start());
168                        let end = cmp::min(range.end(), overlap.end());
169                        needs.entry(*actor_id).or_default().push(SyncNeedV1::Full {
170                            versions: *start..=*end,
171                        })
172                    }
173                }
174            }
175
176            if let Some(our_partials) = self.partial_need.get(actor_id) {
177                for (v, seqs) in our_partials.iter() {
178                    if other_haves.contains(v) {
179                        needs
180                            .entry(*actor_id)
181                            .or_default()
182                            .push(SyncNeedV1::Partial {
183                                version: *v,
184                                seqs: seqs.clone(),
185                            });
186                    } else if let Some(other_seqs) = other
187                        .partial_need
188                        .get(actor_id)
189                        .and_then(|versions| versions.get(v))
190                    {
191                        let max_other_seq = other_seqs.iter().map(|range| *range.end()).max();
192                        let max_our_seq = seqs.iter().map(|range| *range.end()).max();
193
194                        let end_seq = cmp::max(max_other_seq, max_our_seq);
195
196                        if let Some(end) = end_seq {
197                            let mut other_seqs_haves =
198                                RangeInclusiveSet::from_iter([CrsqlSeq(0)..=end]);
199
200                            for seqs in other_seqs.iter() {
201                                other_seqs_haves.remove(seqs.clone());
202                            }
203
204                            let seqs = seqs
205                                .iter()
206                                .flat_map(|range| {
207                                    other_seqs_haves
208                                        .overlapping(range)
209                                        .map(|overlap| {
210                                            let start = cmp::max(range.start(), overlap.start());
211                                            let end = cmp::min(range.end(), overlap.end());
212                                            *start..=*end
213                                        })
214                                        .collect::<Vec<RangeInclusive<CrsqlSeq>>>()
215                                })
216                                .collect::<Vec<RangeInclusive<CrsqlSeq>>>();
217
218                            if !seqs.is_empty() {
219                                needs
220                                    .entry(*actor_id)
221                                    .or_default()
222                                    .push(SyncNeedV1::Partial { version: *v, seqs });
223                            }
224                        }
225                    }
226                }
227            }
228
229            let missing = match self.heads.get(actor_id) {
230                Some(our_head) => {
231                    if head > our_head {
232                        Some((*our_head + 1)..=*head)
233                    } else {
234                        None
235                    }
236                }
237                None => Some(CrsqlDbVersion(1)..=*head),
238            };
239
240            if let Some(missing) = missing {
241                needs
242                    .entry(*actor_id)
243                    .or_default()
244                    .push(SyncNeedV1::Full { versions: missing });
245            }
246        }
247
248        needs
249    }
250}
251
252#[derive(Debug, Clone, PartialEq, Readable, Writable)]
253pub enum SyncNeedV1 {
254    Full {
255        versions: RangeInclusive<CrsqlDbVersion>,
256    },
257    Partial {
258        version: CrsqlDbVersion,
259        seqs: Vec<RangeInclusive<CrsqlSeq>>,
260    },
261    Empty {
262        ts: Option<Timestamp>,
263    },
264}
265
266impl SyncNeedV1 {
267    pub fn count(&self) -> usize {
268        match self {
269            SyncNeedV1::Full { versions } => (versions.end().0 - versions.start().0) as usize + 1,
270            SyncNeedV1::Partial { .. } => 1,
271            SyncNeedV1::Empty { .. } => 1,
272        }
273    }
274}
275
276impl From<SyncStateV1> for SyncMessage {
277    fn from(value: SyncStateV1) -> Self {
278        SyncMessage::V1(SyncMessageV1::State(value))
279    }
280}
281
282// generates a `SyncMessage` to tell another node what versions we're missing
283#[tracing::instrument(skip_all, level = "debug")]
284pub async fn generate_sync(bookie: &Bookie, self_actor_id: ActorId) -> SyncStateV1 {
285    let mut state = SyncStateV1 {
286        actor_id: self_actor_id,
287        ..Default::default()
288    };
289
290    let actors: Vec<(ActorId, Booked)> = {
291        bookie
292            .read::<&str, _>("generate_sync", None)
293            .await
294            .iter()
295            .map(|(k, v)| (*k, v.clone()))
296            .collect()
297    };
298
299    for (actor_id, booked) in actors {
300        let bookedr = booked.read("generate_sync", actor_id.as_simple()).await;
301
302        let last_version = match bookedr.last() {
303            None => continue,
304            Some(v) => v,
305        };
306
307        let need: Vec<_> = bookedr.needed().iter().cloned().collect();
308
309        if !need.is_empty() {
310            state.need.insert(actor_id, need);
311        }
312
313        {
314            for (v, partial) in bookedr
315                .partials
316                .iter()
317                // don't set partial if it is effectively complete
318                .filter(|(_, partial)| !partial.is_complete())
319            {
320                state.partial_need.entry(actor_id).or_default().insert(
321                    *v,
322                    partial
323                        .seqs
324                        .gaps(&(CrsqlSeq(0)..=partial.last_seq))
325                        .collect(),
326                );
327            }
328        }
329        state.heads.insert(actor_id, last_version);
330    }
331
332    state
333}
334
335#[derive(Debug, thiserror::Error)]
336pub enum SyncMessageEncodeError {
337    #[error(transparent)]
338    Encode(#[from] speedy::Error),
339    #[error(transparent)]
340    Io(#[from] io::Error),
341}
342
343#[derive(Debug, thiserror::Error)]
344pub enum SyncMessageDecodeError {
345    #[error(transparent)]
346    Decode(#[from] speedy::Error),
347    #[error("corrupted message, crc mismatch (got: {0}, expected {1})")]
348    Corrupted(u32, u32),
349    #[error(transparent)]
350    Io(#[from] io::Error),
351}
352
353impl SyncMessage {
354    pub fn state(&self) -> Option<&SyncStateV1> {
355        match self {
356            SyncMessage::V1(SyncMessageV1::State(state)) => Some(state),
357            _ => None,
358        }
359    }
360
361    pub fn from_slice<S: AsRef<[u8]>>(slice: S) -> Result<Self, speedy::Error> {
362        Self::read_from_buffer(slice.as_ref())
363    }
364
365    pub fn from_buf(buf: &mut BytesMut) -> Result<Self, SyncMessageDecodeError> {
366        Ok(Self::from_slice(buf)?)
367    }
368
369    pub fn decode(
370        codec: &mut LengthDelimitedCodec,
371        buf: &mut BytesMut,
372    ) -> Result<Option<Self>, SyncMessageDecodeError> {
373        Ok(match codec.decode(buf)? {
374            Some(mut buf) => Some(Self::from_buf(&mut buf)?),
375            None => None,
376        })
377    }
378}
379
380#[cfg(test)]
381mod tests {
382    use uuid::Uuid;
383
384    use super::*;
385
386    #[test]
387    fn test_compute_available_needs() {
388        let actor1 = ActorId(Uuid::new_v4());
389
390        let mut our_state = SyncStateV1::default();
391        our_state.heads.insert(actor1, CrsqlDbVersion(10));
392
393        let mut other_state = SyncStateV1::default();
394        other_state.heads.insert(actor1, CrsqlDbVersion(13));
395
396        assert_eq!(
397            our_state.compute_available_needs(&other_state),
398            [(
399                actor1,
400                vec![SyncNeedV1::Full {
401                    versions: CrsqlDbVersion(11)..=CrsqlDbVersion(13)
402                }]
403            )]
404            .into()
405        );
406
407        our_state
408            .need
409            .entry(actor1)
410            .or_default()
411            .push(CrsqlDbVersion(2)..=CrsqlDbVersion(5));
412        our_state
413            .need
414            .entry(actor1)
415            .or_default()
416            .push(CrsqlDbVersion(7)..=CrsqlDbVersion(7));
417
418        assert_eq!(
419            our_state.compute_available_needs(&other_state),
420            [(
421                actor1,
422                vec![
423                    SyncNeedV1::Full {
424                        versions: CrsqlDbVersion(2)..=CrsqlDbVersion(5)
425                    },
426                    SyncNeedV1::Full {
427                        versions: CrsqlDbVersion(7)..=CrsqlDbVersion(7)
428                    },
429                    SyncNeedV1::Full {
430                        versions: CrsqlDbVersion(11)..=CrsqlDbVersion(13)
431                    }
432                ]
433            )]
434            .into()
435        );
436
437        our_state.partial_need.insert(
438            actor1,
439            [(
440                CrsqlDbVersion(9),
441                vec![CrsqlSeq(100)..=CrsqlSeq(120), CrsqlSeq(130)..=CrsqlSeq(132)],
442            )]
443            .into(),
444        );
445
446        assert_eq!(
447            our_state.compute_available_needs(&other_state),
448            [(
449                actor1,
450                vec![
451                    SyncNeedV1::Full {
452                        versions: CrsqlDbVersion(2)..=CrsqlDbVersion(5)
453                    },
454                    SyncNeedV1::Full {
455                        versions: CrsqlDbVersion(7)..=CrsqlDbVersion(7)
456                    },
457                    SyncNeedV1::Partial {
458                        version: CrsqlDbVersion(9),
459                        seqs: vec![CrsqlSeq(100)..=CrsqlSeq(120), CrsqlSeq(130)..=CrsqlSeq(132)]
460                    },
461                    SyncNeedV1::Full {
462                        versions: CrsqlDbVersion(11)..=CrsqlDbVersion(13)
463                    }
464                ]
465            )]
466            .into()
467        );
468
469        other_state.partial_need.insert(
470            actor1,
471            [(
472                CrsqlDbVersion(9),
473                vec![CrsqlSeq(100)..=CrsqlSeq(110), CrsqlSeq(130)..=CrsqlSeq(130)],
474            )]
475            .into(),
476        );
477
478        assert_eq!(
479            our_state.compute_available_needs(&other_state),
480            [(
481                actor1,
482                vec![
483                    SyncNeedV1::Full {
484                        versions: CrsqlDbVersion(2)..=CrsqlDbVersion(5)
485                    },
486                    SyncNeedV1::Full {
487                        versions: CrsqlDbVersion(7)..=CrsqlDbVersion(7)
488                    },
489                    SyncNeedV1::Partial {
490                        version: CrsqlDbVersion(9),
491                        seqs: vec![CrsqlSeq(111)..=CrsqlSeq(120), CrsqlSeq(131)..=CrsqlSeq(132)]
492                    },
493                    SyncNeedV1::Full {
494                        versions: CrsqlDbVersion(11)..=CrsqlDbVersion(13)
495                    }
496                ]
497            )]
498            .into()
499        );
500    }
501}