1use std::{
4 collections::{BTreeMap, HashMap},
5 sync::Arc,
6 time::{Duration, SystemTime, UNIX_EPOCH},
7};
8
9use futures_util::{StreamExt, future::BoxFuture, stream};
10use inline_sdk::proto;
11use prost::Message as _;
12use tokio::sync::{Mutex, Semaphore};
13
14use crate::{
15 BackendError, BackendResult, ClientErrorCategory, ClientEvent, ClientEventDelivery,
16 ClientStore, InlineId, PendingSyncBatch, StoreError, SyncBucketKey, SyncBucketPeer,
17 SyncBucketState, SyncState,
18};
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub struct SyncConfig {
23 pub last_sync_safety_gap_seconds: i64,
25 pub max_concurrent_bucket_fetches: usize,
27 pub initial_lookback_seconds: i64,
29 pub stale_state_max_age_seconds: i64,
31 pub max_total_updates: i32,
33 pub page_limit: i32,
35 pub cold_start_total_limit: i32,
37 pub inconsistent_empty_retry_attempts: u32,
40 pub inconsistent_empty_retry_delay_ms: u64,
42}
43
44impl Default for SyncConfig {
45 fn default() -> Self {
46 Self {
47 last_sync_safety_gap_seconds: 15,
48 max_concurrent_bucket_fetches: 4,
49 initial_lookback_seconds: 5 * 24 * 60 * 60,
50 stale_state_max_age_seconds: 14 * 24 * 60 * 60,
51 max_total_updates: 1_000,
52 page_limit: 200,
53 cold_start_total_limit: 50,
54 inconsistent_empty_retry_attempts: 3,
55 inconsistent_empty_retry_delay_ms: 250,
56 }
57 }
58}
59
60pub(crate) trait SyncHost: Clone + Send + Sync + 'static {
61 fn get_updates_state(
62 &self,
63 date: i64,
64 ) -> BoxFuture<'static, BackendResult<proto::GetUpdatesStateResult>>;
65
66 fn get_updates(
67 &self,
68 input: proto::GetUpdatesInput,
69 ) -> BoxFuture<'static, BackendResult<proto::GetUpdatesResult>>;
70
71 fn apply_sync_batch(
72 &self,
73 updates: Vec<proto::Update>,
74 sidecars: Option<proto::UpdateSidecars>,
75 ) -> BoxFuture<'static, BackendResult<Vec<ClientEvent>>>;
76
77 fn repair_bucket(
80 &self,
81 key: SyncBucketKey,
82 ) -> BoxFuture<'static, BackendResult<Vec<ClientEvent>>>;
83}
84
85#[derive(Debug)]
86pub(crate) struct SyncManager {
87 store: Arc<dyn ClientStore>,
88 config: SyncConfig,
89 bucket_locks: Mutex<HashMap<SyncBucketKey, Arc<Mutex<()>>>>,
90 fetch_limiter: Arc<Semaphore>,
91 sync_state_lock: Mutex<()>,
92}
93
94impl SyncManager {
95 pub(crate) fn new(store: Arc<dyn ClientStore>, config: SyncConfig) -> Self {
96 let max_fetches = config.max_concurrent_bucket_fetches.max(1);
97 Self {
98 store,
99 config,
100 bucket_locks: Mutex::new(HashMap::new()),
101 fetch_limiter: Arc::new(Semaphore::new(max_fetches)),
102 sync_state_lock: Mutex::new(()),
103 }
104 }
105
106 pub(crate) async fn discover<H: SyncHost>(
107 &self,
108 host: &H,
109 ) -> BackendResult<Vec<ClientEventDelivery>> {
110 let mut events = self.recover_pending_batches(host).await?;
111 let state = self.prepared_sync_state().await?;
112 log::debug!(
113 "starting Inline update discovery from date={}",
114 state.last_sync_date
115 );
116 let result = host.get_updates_state(state.last_sync_date).await?;
117 if result.updates_found == Some(false) {
118 self.update_last_sync_date(result.date).await?;
119 }
120 events.extend(
121 self.process_bucket(host, SyncBucketKey::User, Vec::new(), None, true)
122 .await?,
123 );
124 log::debug!(
125 "finished Inline update discovery date={} events={}",
126 result.date,
127 events.len()
128 );
129 Ok(events)
130 }
131
132 pub(crate) async fn process_realtime<H: SyncHost>(
133 &self,
134 host: &H,
135 updates: Vec<proto::Update>,
136 ) -> BackendResult<Vec<ClientEventDelivery>> {
137 let mut events = self.recover_pending_batches(host).await?;
138 let received_count = updates.len();
139 let mut direct = Vec::new();
140 let mut buckets = HashMap::<SyncBucketKey, Vec<proto::Update>>::new();
141 let mut targets = HashMap::<SyncBucketKey, i64>::new();
142
143 for update in updates {
144 match update.update.as_ref() {
145 Some(proto::update::Update::ChatHasNewUpdates(hint)) => {
146 if let Some(key) = chat_hint_bucket_key(hint) {
147 targets
148 .entry(key)
149 .and_modify(|seq| *seq = (*seq).max(i64::from(hint.update_seq)))
150 .or_insert(i64::from(hint.update_seq));
151 }
152 }
153 Some(proto::update::Update::SpaceHasNewUpdates(hint)) => {
154 let key = SyncBucketKey::Space {
155 space_id: InlineId::new(hint.space_id),
156 };
157 targets
158 .entry(key)
159 .and_modify(|seq| *seq = (*seq).max(i64::from(hint.update_seq)))
160 .or_insert(i64::from(hint.update_seq));
161 }
162 _ => {
163 let seq = update_seq(&update);
164 if seq > 0
165 && let Some(key) = bucket_key_for_update(&update)
166 {
167 buckets.entry(key).or_default().push(update);
168 } else {
169 direct.push(update);
170 }
171 }
172 }
173 }
174
175 if !direct.is_empty() {
176 let max_date = max_update_date(&direct);
177 let applied = host.apply_sync_batch(direct, None).await?;
178 let applied = self
179 .store
180 .append_client_events(applied)
181 .await
182 .map_err(store_error_to_backend)?;
183 self.update_last_sync_date(max_date).await?;
184 events.extend(applied);
185 }
186
187 for key in targets.keys() {
188 buckets.entry(*key).or_default();
189 }
190
191 let mut ordered = buckets
192 .into_iter()
193 .map(|(key, updates)| {
194 let target = targets.get(&key).copied();
195 (key, updates, target)
196 })
197 .collect::<Vec<_>>();
198 let bucket_count = ordered.len();
199 ordered.sort_by_key(|(key, _, _)| bucket_sort_key(*key));
200
201 if let Some(user_index) = ordered
202 .iter()
203 .position(|(key, _, _)| *key == SyncBucketKey::User)
204 {
205 let (key, updates, target) = ordered.remove(user_index);
206 events.extend(
207 self.process_bucket(host, key, updates, target, false)
208 .await?,
209 );
210 }
211
212 let concurrency = self.config.max_concurrent_bucket_fetches.max(1);
213 let mut results = stream::iter(ordered)
214 .map(|(key, updates, target)| async move {
215 (
216 bucket_sort_key(key),
217 self.process_bucket(host, key, updates, target, false).await,
218 )
219 })
220 .buffer_unordered(concurrency)
221 .collect::<Vec<_>>()
222 .await;
223 results.sort_by_key(|(sort_key, _)| *sort_key);
224 for (_, result) in results {
225 events.extend(result?);
226 }
227 log::debug!(
228 "processed Inline realtime batch received={received_count} buckets={bucket_count} events={}",
229 events.len()
230 );
231 Ok(events)
232 }
233
234 async fn process_bucket<H: SyncHost>(
235 &self,
236 host: &H,
237 key: SyncBucketKey,
238 realtime_updates: Vec<proto::Update>,
239 target_seq: Option<i64>,
240 force_fetch: bool,
241 ) -> BackendResult<Vec<ClientEventDelivery>> {
242 let bucket_lock = self.bucket_lock(key).await;
243 let _bucket_guard = bucket_lock.lock().await;
244 let mut state = self
245 .store
246 .sync_bucket_state(key)
247 .await
248 .map_err(store_error_to_backend)?;
249 let mut buffered = BTreeMap::new();
250 for update in realtime_updates {
251 let seq = update_seq(&update);
252 if seq > state.seq {
253 insert_unique_update(&mut buffered, seq, update, "realtime buffer")?;
254 }
255 }
256
257 let mut events = self
258 .drain_contiguous(host, key, &mut state, &mut buffered)
259 .await?;
260 let target_seq = target_seq
261 .filter(|target| *target > state.seq)
262 .or_else(|| buffered.last_key_value().map(|(seq, _)| *seq));
263 if !force_fetch && target_seq.is_none() && buffered.is_empty() && state.seq > 0 {
264 return Ok(events);
265 }
266
267 events.extend(
268 self.fetch_bucket(host, key, state, buffered, target_seq)
269 .await?,
270 );
271 Ok(events)
272 }
273
274 async fn drain_contiguous<H: SyncHost>(
275 &self,
276 host: &H,
277 key: SyncBucketKey,
278 state: &mut SyncBucketState,
279 buffered: &mut BTreeMap<i64, proto::Update>,
280 ) -> BackendResult<Vec<ClientEventDelivery>> {
281 let mut updates = Vec::new();
282 let mut next_seq = state.seq + 1;
283 while let Some(update) = buffered.remove(&next_seq) {
284 updates.push(update);
285 next_seq += 1;
286 }
287 if updates.is_empty() {
288 return Ok(Vec::new());
289 }
290
291 let committed = SyncBucketState {
292 seq: next_seq - 1,
293 date: state.date.max(max_update_date(&updates)),
294 };
295 let events = self
296 .commit_sync_batch(host, key, committed, updates, None)
297 .await?;
298 self.update_last_sync_date(committed.date).await?;
299 *state = committed;
300 Ok(events)
301 }
302
303 async fn fetch_bucket<H: SyncHost>(
304 &self,
305 host: &H,
306 key: SyncBucketKey,
307 initial: SyncBucketState,
308 buffered: BTreeMap<i64, proto::Update>,
309 target_seq: Option<i64>,
310 ) -> BackendResult<Vec<ClientEventDelivery>> {
311 let _permit = self
312 .fetch_limiter
313 .clone()
314 .acquire_owned()
315 .await
316 .map_err(|_| BackendError::new(ClientErrorCategory::Internal, "sync fetch stopped"))?;
317 let mut cold_start = initial.seq == 0 || initial.date == 0;
318 let mut committed_floor = initial.seq;
319 let mut current_seq = initial.seq;
320 let mut final_date = initial.date;
321 let hard_end = target_seq.or_else(|| buffered.last_key_value().map(|(seq, _)| *seq));
322 let mut buffered = buffered;
323 let mut slice_end = None;
324 let mut fetched = BTreeMap::<i64, proto::Update>::new();
325 let mut sidecars = proto::UpdateSidecars::default();
326 let mut page_count = 0_u64;
327 let mut inconsistent_empty_attempts = 0_u32;
328 let mut deliveries = Vec::new();
329
330 log::debug!(
331 "starting Inline bucket fetch kind={} start_seq={} target_seq={:?} cold_start={cold_start}",
332 bucket_kind(key),
333 current_seq,
334 hard_end
335 );
336
337 loop {
338 page_count += 1;
339 let request_end = slice_end.or(hard_end).filter(|end| *end > current_seq);
340 let response = host
341 .get_updates(proto::GetUpdatesInput {
342 bucket: Some(protocol_bucket(key)),
343 start_seq: current_seq,
344 total_limit: if cold_start && slice_end.is_none() {
345 self.config.cold_start_total_limit
346 } else {
347 self.config.max_total_updates
348 },
349 seq_end: request_end.unwrap_or_default(),
350 limit: self.config.page_limit,
351 core_sync_schema_revision: crate::CORE_SYNC_SCHEMA_REVISION,
352 })
353 .await?;
354 let result_type = proto::get_updates_result::ResultType::try_from(response.result_type)
355 .unwrap_or(proto::get_updates_result::ResultType::Unspecified);
356
357 if result_type == proto::get_updates_result::ResultType::TooLong {
358 if response.seq <= current_seq {
359 return Err(BackendError::new(
360 ClientErrorCategory::ProtocolMismatch,
361 "bucket sync received a non-advancing TOO_LONG pointer",
362 ));
363 }
364 if cold_start {
365 let target_seq = response.seq;
366 let target_date = final_date.max(response.date);
367 log::warn!(
368 "repairing cold Inline bucket after TOO_LONG kind={} target_seq={target_seq}",
369 bucket_kind(key)
370 );
371 deliveries.extend(
372 self.repair_cold_bucket(host, key, target_seq, target_date)
373 .await?,
374 );
375 cold_start = false;
376 committed_floor = target_seq;
377 current_seq = target_seq;
378 final_date = target_date;
379 fetched.clear();
380 sidecars = proto::UpdateSidecars::default();
381 slice_end = None;
382 buffered.retain(|seq, _| *seq > target_seq);
383 if hard_end.is_none_or(|end| target_seq >= end) {
384 return Ok(deliveries);
385 }
386 continue;
387 }
388 let max_slice = current_seq + i64::from(self.config.max_total_updates);
389 slice_end = Some(response.seq.min(max_slice));
390 continue;
391 }
392
393 if response.seq < current_seq {
394 return Err(BackendError::new(
395 ClientErrorCategory::ProtocolMismatch,
396 "bucket sync server cursor moved backwards",
397 ));
398 }
399 let previous_seq = current_seq;
400 let accounting = validate_response_page(previous_seq, &response)?;
401 current_seq = response.seq;
402 final_date = final_date.max(response.date);
403 let skipped_sequences = response
404 .skipped_sequences
405 .iter()
406 .map(|skipped| skipped.seq)
407 .collect::<std::collections::BTreeSet<_>>();
408 let page_sidecars = response.sidecars;
409 let page_updates = response.updates;
410
411 let empty = result_type == proto::get_updates_result::ResultType::Empty;
412 if accounting.requires_snapshot_repair {
413 log::warn!(
414 "repairing Inline bucket after server-classified snapshot gap kind={} target_seq={current_seq}",
415 bucket_kind(key)
416 );
417 deliveries.extend(
418 self.repair_cold_bucket(host, key, current_seq, final_date)
419 .await?,
420 );
421 return Ok(deliveries);
422 }
423
424 if cold_start {
425 if let Some(page_sidecars) = page_sidecars {
426 merge_sidecars(&mut sidecars, page_sidecars);
427 }
428 for update in page_updates {
429 let seq = update_seq(&update);
430 if seq > committed_floor {
431 insert_unique_update(&mut fetched, seq, update, "fetched page")?;
432 }
433 }
434 } else if current_seq > previous_seq {
435 let mut page = BTreeMap::<i64, proto::Update>::new();
436 for update in page_updates {
437 let seq = update_seq(&update);
438 insert_unique_update(&mut page, seq, update, "fetched page")?;
439 }
440 let buffered_sequences = buffered
441 .range(..=current_seq)
442 .map(|(seq, _)| *seq)
443 .collect::<Vec<_>>();
444 for seq in &buffered_sequences {
445 if skipped_sequences.contains(seq) {
446 return Err(BackendError::new(
447 ClientErrorCategory::ProtocolMismatch,
448 "realtime update conflicted with a server-classified skipped sequence",
449 ));
450 }
451 if let Some(update) = buffered.get(seq).cloned() {
452 insert_unique_update(&mut page, *seq, update, "fetched/realtime merge")?;
453 }
454 }
455 let updates = page.into_values().collect::<Vec<_>>();
456 let committed = SyncBucketState {
457 seq: current_seq,
458 date: final_date.max(max_update_date(&updates)),
459 };
460 let has_sidecars = page_sidecars.as_ref().is_some_and(has_sidecars);
461 let page_deliveries = self
462 .commit_sync_batch(
463 host,
464 key,
465 committed,
466 updates,
467 has_sidecars.then_some(page_sidecars).flatten(),
468 )
469 .await?;
470 self.update_last_sync_date(committed.date).await?;
471 deliveries.extend(page_deliveries);
472 for seq in buffered_sequences {
473 buffered.remove(&seq);
474 }
475 committed_floor = committed.seq;
476 }
477 let final_page = response.r#final.unwrap_or(false) || empty;
478 if final_page && hard_end.is_some_and(|target| current_seq < target) {
479 if current_seq > previous_seq {
480 inconsistent_empty_attempts = 0;
481 }
482 inconsistent_empty_attempts += 1;
483 if inconsistent_empty_attempts <= self.config.inconsistent_empty_retry_attempts {
484 log::warn!(
485 "retrying Inline final page behind hint kind={} server_seq={} target_seq={:?} attempt={}",
486 bucket_kind(key),
487 current_seq,
488 hard_end,
489 inconsistent_empty_attempts
490 );
491 if self.config.inconsistent_empty_retry_delay_ms > 0 {
492 tokio::time::sleep(Duration::from_millis(
493 self.config.inconsistent_empty_retry_delay_ms,
494 ))
495 .await;
496 }
497 continue;
498 }
499 return Err(BackendError::new(
500 ClientErrorCategory::ProtocolMismatch,
501 "bucket sync final response remained behind the requested target",
502 ));
503 }
504 inconsistent_empty_attempts = 0;
505 if current_seq == previous_seq && !final_page {
506 return Err(BackendError::new(
507 ClientErrorCategory::ProtocolMismatch,
508 "bucket sync received a non-progress response",
509 ));
510 }
511 if let Some(end) = slice_end
512 && current_seq >= end
513 {
514 slice_end = None;
515 if hard_end.is_some_and(|target| current_seq < target) {
516 continue;
517 }
518 }
519 if hard_end.is_some_and(|target| current_seq >= target) || final_page {
520 break;
521 }
522 }
523
524 if !cold_start {
525 log::debug!(
526 "finished incremental Inline bucket fetch kind={} pages={page_count} end_seq={} events={}",
527 bucket_kind(key),
528 committed_floor,
529 deliveries.len()
530 );
531 return Ok(deliveries);
532 }
533
534 for (seq, update) in buffered {
535 if seq > committed_floor && seq <= current_seq {
536 insert_unique_update(&mut fetched, seq, update, "fetched/realtime merge")?;
537 }
538 }
539 let updates = fetched.into_values().collect::<Vec<_>>();
540 let max_delivered_seq = updates.iter().map(update_seq).max().unwrap_or(initial.seq);
541 if current_seq > max_delivered_seq {
542 log::warn!(
543 "advancing cold Inline bucket pointer with explicit sequence accounting kind={} delivered_seq={max_delivered_seq} pointer_seq={current_seq}",
544 bucket_kind(key)
545 );
546 }
547 let committed = SyncBucketState {
548 seq: current_seq,
549 date: final_date.max(max_update_date(&updates)),
550 };
551 if cold_start {
552 let events = self
553 .repair_cold_bucket(host, key, committed.seq, committed.date)
554 .await?;
555 log::debug!(
556 "finished cold Inline bucket snapshot kind={} pages={page_count} end_seq={} events={}",
557 bucket_kind(key),
558 committed.seq,
559 events.len()
560 );
561 deliveries.extend(events);
562 return Ok(deliveries);
563 }
564 let has_sidecars = has_sidecars(&sidecars);
565 let update_count = updates.len();
566 let events = self
567 .commit_sync_batch(
568 host,
569 key,
570 committed,
571 updates,
572 has_sidecars.then_some(sidecars),
573 )
574 .await?;
575 self.update_last_sync_date(committed.date).await?;
576 log::debug!(
577 "finished Inline bucket fetch kind={} pages={page_count} end_seq={} updates={} events={}",
578 bucket_kind(key),
579 committed.seq,
580 update_count,
581 events.len()
582 );
583 deliveries.extend(events);
584 Ok(deliveries)
585 }
586
587 async fn repair_cold_bucket<H: SyncHost>(
588 &self,
589 host: &H,
590 key: SyncBucketKey,
591 target_seq: i64,
592 target_date: i64,
593 ) -> BackendResult<Vec<ClientEventDelivery>> {
594 let events = host.repair_bucket(key).await?;
595 self.commit_cold_pointer(key, target_seq, target_date, events)
596 .await
597 }
598
599 async fn commit_cold_pointer(
600 &self,
601 key: SyncBucketKey,
602 target_seq: i64,
603 target_date: i64,
604 events: Vec<ClientEvent>,
605 ) -> BackendResult<Vec<ClientEventDelivery>> {
606 let committed = SyncBucketState {
607 seq: target_seq,
608 date: target_date,
609 };
610 let deliveries = self
614 .store
615 .commit_pending_sync_batch_with_events(key, committed, events)
616 .await
617 .map_err(store_error_to_backend)?;
618 self.update_last_sync_date(committed.date).await?;
619 Ok(deliveries)
620 }
621
622 async fn commit_sync_batch<H: SyncHost>(
623 &self,
624 host: &H,
625 key: SyncBucketKey,
626 committed_state: SyncBucketState,
627 updates: Vec<proto::Update>,
628 sidecars: Option<proto::UpdateSidecars>,
629 ) -> BackendResult<Vec<ClientEventDelivery>> {
630 validate_journal_updates(&updates)?;
631 let payload = encode_pending_payload(&updates, sidecars.as_ref())?;
632 self.store
633 .save_pending_sync_batch(PendingSyncBatch {
634 key,
635 committed_state,
636 payload,
637 })
638 .await
639 .map_err(store_error_to_backend)?;
640 let events = match host.apply_sync_batch(updates, sidecars).await {
641 Ok(events) => events,
642 Err(error) => {
643 if is_permanent_sync_error(&error) {
644 self.store
645 .discard_pending_sync_batch(key)
646 .await
647 .map_err(store_error_to_backend)?;
648 }
649 return Err(error);
650 }
651 };
652 let deliveries = self
653 .store
654 .commit_pending_sync_batch_with_events(key, committed_state, events)
655 .await
656 .map_err(store_error_to_backend)?;
657 Ok(deliveries)
658 }
659
660 async fn recover_pending_batches<H: SyncHost>(
661 &self,
662 host: &H,
663 ) -> BackendResult<Vec<ClientEventDelivery>> {
664 let batches = self
665 .store
666 .pending_sync_batches()
667 .await
668 .map_err(store_error_to_backend)?;
669 let mut events = Vec::new();
670 let mut refetch = BTreeMap::<(u8, i64), (SyncBucketKey, i64)>::new();
671 for batch in batches {
672 let decoded = decode_pending_payload(&batch.payload).and_then(|(updates, sidecars)| {
673 validate_journal_updates(&updates)?;
674 Ok((updates, sidecars))
675 });
676 let (updates, sidecars) = match decoded {
677 Ok(decoded) => decoded,
678 Err(error) if is_permanent_sync_error(&error) => {
679 log::warn!(
680 "discarding incompatible Inline sync journal kind={} target_seq={}",
681 bucket_kind(batch.key),
682 batch.committed_state.seq
683 );
684 self.store
685 .discard_pending_sync_batch(batch.key)
686 .await
687 .map_err(store_error_to_backend)?;
688 refetch.insert(
689 bucket_sort_key(batch.key),
690 (batch.key, batch.committed_state.seq),
691 );
692 continue;
693 }
694 Err(error) => return Err(error),
695 };
696 let applied = match host.apply_sync_batch(updates, sidecars).await {
697 Ok(applied) => applied,
698 Err(error) if is_permanent_sync_error(&error) => {
699 log::warn!(
700 "discarding permanently failing Inline sync journal kind={} target_seq={}",
701 bucket_kind(batch.key),
702 batch.committed_state.seq
703 );
704 self.store
705 .discard_pending_sync_batch(batch.key)
706 .await
707 .map_err(store_error_to_backend)?;
708 refetch.insert(
709 bucket_sort_key(batch.key),
710 (batch.key, batch.committed_state.seq),
711 );
712 continue;
713 }
714 Err(error) => return Err(error),
715 };
716 events.extend(
717 self.store
718 .commit_pending_sync_batch_with_events(
719 batch.key,
720 batch.committed_state,
721 applied,
722 )
723 .await
724 .map_err(store_error_to_backend)?,
725 );
726 self.update_last_sync_date(batch.committed_state.date)
727 .await?;
728 }
729 for (_, (key, target_seq)) in refetch {
730 events.extend(
731 self.process_bucket(host, key, Vec::new(), Some(target_seq), true)
732 .await?,
733 );
734 }
735 Ok(events)
736 }
737
738 async fn bucket_lock(&self, key: SyncBucketKey) -> Arc<Mutex<()>> {
739 let mut locks = self.bucket_locks.lock().await;
740 locks
741 .entry(key)
742 .or_insert_with(|| Arc::new(Mutex::new(())))
743 .clone()
744 }
745
746 async fn prepared_sync_state(&self) -> BackendResult<SyncState> {
747 let mut state = self
748 .store
749 .sync_state()
750 .await
751 .map_err(store_error_to_backend)?;
752 let now = now_seconds();
753 if state.last_sync_date == 0
754 || now.saturating_sub(state.last_sync_date) > self.config.stale_state_max_age_seconds
755 {
756 state.last_sync_date = now.saturating_sub(self.config.initial_lookback_seconds);
757 self.store
758 .save_sync_state(state)
759 .await
760 .map_err(store_error_to_backend)?;
761 }
762 Ok(state)
763 }
764
765 async fn update_last_sync_date(&self, max_applied_date: i64) -> BackendResult<()> {
766 if max_applied_date <= 0 {
767 return Ok(());
768 }
769 let _guard = self.sync_state_lock.lock().await;
770 let current = self
771 .store
772 .sync_state()
773 .await
774 .map_err(store_error_to_backend)?;
775 let proposed = max_applied_date.saturating_sub(self.config.last_sync_safety_gap_seconds);
776 if proposed <= current.last_sync_date {
777 return Ok(());
778 }
779 self.store
780 .save_sync_state(SyncState {
781 last_sync_date: proposed,
782 })
783 .await
784 .map_err(store_error_to_backend)
785 }
786}
787
788fn bucket_key_for_update(update: &proto::Update) -> Option<SyncBucketKey> {
789 use proto::update::Update;
790 match update.update.as_ref()? {
791 Update::NewMessage(value) => value
792 .message
793 .as_ref()?
794 .peer_id
795 .as_ref()
796 .and_then(chat_bucket),
797 Update::EditMessage(value) => value
798 .message
799 .as_ref()?
800 .peer_id
801 .as_ref()
802 .and_then(chat_bucket),
803 Update::DeleteMessages(value) => value.peer_id.as_ref().and_then(chat_bucket),
804 Update::ClearChatHistory(value) => match value.target.as_ref()? {
805 proto::update_clear_chat_history::Target::PeerId(peer) => chat_bucket(peer),
806 proto::update_clear_chat_history::Target::SpaceId(space_id) => {
807 Some(SyncBucketKey::Space {
808 space_id: InlineId::new(*space_id),
809 })
810 }
811 },
812 Update::MessageAttachment(value) => value.peer_id.as_ref().and_then(chat_bucket),
813 Update::UpdateReaction(value) => value
814 .reaction
815 .as_ref()
816 .map(|reaction| chat_key(reaction.chat_id)),
817 Update::DeleteReaction(value) => Some(chat_key(value.chat_id)),
818 Update::DeleteChat(value) => value.peer_id.as_ref().and_then(chat_bucket),
819 Update::MarkAsUnread(value) => value.peer_id.as_ref().and_then(chat_bucket),
820 Update::SpaceMemberAdd(value) => value
821 .member
822 .as_ref()
823 .map(|member| space_key(member.space_id)),
824 Update::SpaceMemberDelete(value) => Some(space_key(value.space_id)),
825 Update::SpaceMemberUpdate(value) => value
826 .member
827 .as_ref()
828 .map(|member| space_key(member.space_id)),
829 Update::JoinSpace(_)
830 | Update::UpdateUserStatus(_)
831 | Update::UpdateUserSettings(_)
832 | Update::UpdatedUser(_)
833 | Update::DialogArchived(_)
834 | Update::DialogNotificationSettings(_)
835 | Update::DialogFollowMode(_)
836 | Update::UpdateReadMaxId(_)
837 | Update::ChatOpen(_) => Some(SyncBucketKey::User),
838 Update::NewChat(value) => value.chat.as_ref()?.peer_id.as_ref().and_then(chat_bucket),
839 Update::ChatMoved(value) => value.chat.as_ref()?.peer_id.as_ref().and_then(chat_bucket),
840 Update::ParticipantAdd(value) => Some(chat_key(value.chat_id)),
841 Update::ParticipantDelete(value) => Some(chat_key(value.chat_id)),
842 Update::ParticipantGroupAdd(value) => Some(chat_key(value.chat_id)),
843 Update::ParticipantGroupDelete(value) => Some(chat_key(value.chat_id)),
844 Update::ChatVisibility(value) => Some(chat_key(value.chat_id)),
845 Update::ChatInfo(value) => Some(chat_key(value.chat_id)),
846 Update::PinnedMessages(value) => value.peer_id.as_ref().and_then(chat_bucket),
847 Update::ChatSkipPts(value) => Some(chat_key(value.chat_id)),
848 Update::SpaceSettings(value) => Some(space_key(value.space_id)),
849 _ => None,
850 }
851}
852
853fn chat_hint_bucket_key(hint: &proto::UpdateChatHasNewUpdates) -> Option<SyncBucketKey> {
854 hint.peer_id
855 .as_ref()
856 .and_then(chat_bucket)
857 .or_else(|| (hint.chat_id != 0).then(|| chat_key(hint.chat_id)))
858}
859
860fn chat_bucket(peer: &proto::Peer) -> Option<SyncBucketKey> {
861 let peer = match peer.r#type.as_ref()? {
862 proto::peer::Type::User(user) => SyncBucketPeer::User {
863 user_id: InlineId::new(user.user_id),
864 },
865 proto::peer::Type::Chat(chat) => SyncBucketPeer::Chat {
866 chat_id: InlineId::new(chat.chat_id),
867 },
868 };
869 Some(SyncBucketKey::Chat { peer })
870}
871
872fn chat_key(chat_id: i64) -> SyncBucketKey {
873 SyncBucketKey::Chat {
874 peer: SyncBucketPeer::Chat {
875 chat_id: InlineId::new(chat_id),
876 },
877 }
878}
879
880fn space_key(space_id: i64) -> SyncBucketKey {
881 SyncBucketKey::Space {
882 space_id: InlineId::new(space_id),
883 }
884}
885
886fn protocol_bucket(key: SyncBucketKey) -> proto::UpdateBucket {
887 let r#type = match key {
888 SyncBucketKey::User => proto::update_bucket::Type::User(proto::UpdateBucketUser {}),
889 SyncBucketKey::Space { space_id } => {
890 proto::update_bucket::Type::Space(proto::UpdateBucketSpace {
891 space_id: space_id.get(),
892 })
893 }
894 SyncBucketKey::Chat { peer } => {
895 let r#type = match peer {
896 SyncBucketPeer::User { user_id } => {
897 proto::input_peer::Type::User(proto::InputPeerUser {
898 user_id: user_id.get(),
899 })
900 }
901 SyncBucketPeer::Chat { chat_id } => {
902 proto::input_peer::Type::Chat(proto::InputPeerChat {
903 chat_id: chat_id.get(),
904 })
905 }
906 };
907 proto::update_bucket::Type::Chat(proto::UpdateBucketChat {
908 peer_id: Some(proto::InputPeer {
909 r#type: Some(r#type),
910 }),
911 })
912 }
913 };
914 proto::UpdateBucket {
915 r#type: Some(r#type),
916 }
917}
918
919fn update_seq(update: &proto::Update) -> i64 {
920 i64::from(update.seq.unwrap_or_default())
921}
922
923#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
924struct PageAccounting {
925 requires_snapshot_repair: bool,
926}
927
928fn validate_response_page(
929 previous_seq: i64,
930 response: &proto::GetUpdatesResult,
931) -> BackendResult<PageAccounting> {
932 use proto::get_updates_result::ResultType;
933 use proto::sync_skipped_sequence::Reason;
934
935 let result_type = ResultType::try_from(response.result_type).map_err(|_| {
936 BackendError::new(
937 ClientErrorCategory::ProtocolMismatch,
938 "bucket sync response used an unknown result type",
939 )
940 })?;
941 if !matches!(result_type, ResultType::Slice | ResultType::Empty) {
942 return Err(BackendError::new(
943 ClientErrorCategory::ProtocolMismatch,
944 "bucket sync response used an invalid page result type",
945 ));
946 }
947 if response.seq < previous_seq {
948 return Err(BackendError::new(
949 ClientErrorCategory::ProtocolMismatch,
950 "bucket sync page cursor moved backwards",
951 ));
952 }
953
954 let mut accounted = BTreeMap::<i64, &'static str>::new();
955 for update in &response.updates {
956 let seq = update_seq(update);
957 validate_page_sequence(previous_seq, response.seq, seq)?;
958 if accounted.insert(seq, "update").is_some() {
959 return Err(BackendError::new(
960 ClientErrorCategory::ProtocolMismatch,
961 "bucket sync page contained a duplicate sequence",
962 ));
963 }
964 }
965
966 let mut requires_snapshot_repair = false;
967 for skipped in &response.skipped_sequences {
968 validate_page_sequence(previous_seq, response.seq, skipped.seq)?;
969 let reason = Reason::try_from(skipped.reason).map_err(|_| {
970 BackendError::new(
971 ClientErrorCategory::ProtocolMismatch,
972 "bucket sync page used an unknown skipped-sequence reason",
973 )
974 })?;
975 match reason {
976 Reason::IrrelevantToBucket => {}
977 Reason::SnapshotRepairRequired => requires_snapshot_repair = true,
978 Reason::Unspecified => {
979 return Err(BackendError::new(
980 ClientErrorCategory::ProtocolMismatch,
981 "bucket sync page used an unspecified skipped-sequence reason",
982 ));
983 }
984 }
985 if accounted.insert(skipped.seq, "skip").is_some() {
986 return Err(BackendError::new(
987 ClientErrorCategory::ProtocolMismatch,
988 "bucket sync page accounted for one sequence more than once",
989 ));
990 }
991 }
992
993 let delta = response.seq.saturating_sub(previous_seq);
994 let accounted_count = i64::try_from(accounted.len()).map_err(|_| {
995 BackendError::new(
996 ClientErrorCategory::ProtocolMismatch,
997 "bucket sync page sequence count overflowed",
998 )
999 })?;
1000 if accounted_count != delta {
1001 return Err(BackendError::new(
1002 ClientErrorCategory::ProtocolMismatch,
1003 "bucket sync page did not account for every advanced sequence",
1004 ));
1005 }
1006
1007 Ok(PageAccounting {
1008 requires_snapshot_repair,
1009 })
1010}
1011
1012fn validate_page_sequence(previous_seq: i64, response_seq: i64, seq: i64) -> BackendResult<()> {
1013 if seq <= previous_seq || seq > response_seq {
1014 return Err(BackendError::new(
1015 ClientErrorCategory::ProtocolMismatch,
1016 "bucket sync page contained a sequence outside its cursor envelope",
1017 ));
1018 }
1019 Ok(())
1020}
1021
1022fn insert_unique_update(
1023 updates: &mut BTreeMap<i64, proto::Update>,
1024 seq: i64,
1025 update: proto::Update,
1026 source: &'static str,
1027) -> BackendResult<()> {
1028 if let Some(existing) = updates.get(&seq) {
1029 if existing == &update {
1030 return Ok(());
1031 }
1032 return Err(BackendError::new(
1033 ClientErrorCategory::ProtocolMismatch,
1034 format!("conflicting Inline updates shared sequence {seq} in {source}"),
1035 ));
1036 }
1037 updates.insert(seq, update);
1038 Ok(())
1039}
1040
1041fn validate_journal_updates(updates: &[proto::Update]) -> BackendResult<()> {
1042 for update in updates {
1043 if update_seq(update) <= 0 {
1044 return Err(BackendError::new(
1045 ClientErrorCategory::ProtocolMismatch,
1046 "lossless sync journal contained an unsequenced update",
1047 ));
1048 }
1049 match update.update.as_ref() {
1050 Some(proto::update::Update::ChatHasNewUpdates(_))
1051 | Some(proto::update::Update::SpaceHasNewUpdates(_)) => {
1052 return Err(BackendError::new(
1053 ClientErrorCategory::ProtocolMismatch,
1054 "lossless sync journal contained a realtime hint",
1055 ));
1056 }
1057 Some(_) => {}
1058 None => {
1059 return Err(BackendError::new(
1060 ClientErrorCategory::ProtocolMismatch,
1061 "lossless sync journal contained an unknown or missing update payload",
1062 ));
1063 }
1064 }
1065 }
1066 Ok(())
1067}
1068
1069fn is_permanent_sync_error(error: &BackendError) -> bool {
1070 matches!(
1071 error.category,
1072 ClientErrorCategory::ProtocolMismatch | ClientErrorCategory::Unsupported
1073 )
1074}
1075
1076fn max_update_date(updates: &[proto::Update]) -> i64 {
1077 updates
1078 .iter()
1079 .filter_map(|update| update.date)
1080 .max()
1081 .unwrap_or_default()
1082}
1083
1084fn merge_sidecars(target: &mut proto::UpdateSidecars, page: proto::UpdateSidecars) {
1085 target.users.extend(page.users);
1086 target.chats.extend(page.chats);
1087 target.dialogs.extend(page.dialogs);
1088 target.spaces.extend(page.spaces);
1089 target.user_groups.extend(page.user_groups);
1090}
1091
1092fn has_sidecars(sidecars: &proto::UpdateSidecars) -> bool {
1093 !sidecars.users.is_empty()
1094 || !sidecars.chats.is_empty()
1095 || !sidecars.dialogs.is_empty()
1096 || !sidecars.spaces.is_empty()
1097 || !sidecars.user_groups.is_empty()
1098}
1099
1100fn bucket_sort_key(key: SyncBucketKey) -> (u8, i64) {
1101 match key {
1102 SyncBucketKey::User => (0, 0),
1103 SyncBucketKey::Space { space_id } => (1, space_id.get()),
1104 SyncBucketKey::Chat {
1105 peer: SyncBucketPeer::User { user_id },
1106 } => (2, user_id.get()),
1107 SyncBucketKey::Chat {
1108 peer: SyncBucketPeer::Chat { chat_id },
1109 } => (3, chat_id.get()),
1110 }
1111}
1112
1113fn bucket_kind(key: SyncBucketKey) -> &'static str {
1114 match key {
1115 SyncBucketKey::User => "user",
1116 SyncBucketKey::Space { .. } => "space",
1117 SyncBucketKey::Chat { .. } => "chat",
1118 }
1119}
1120
1121fn store_error_to_backend(error: StoreError) -> BackendError {
1122 BackendError::new(error.category, error.message)
1123}
1124
1125fn now_seconds() -> i64 {
1126 SystemTime::now()
1127 .duration_since(UNIX_EPOCH)
1128 .unwrap_or_default()
1129 .as_secs() as i64
1130}
1131
1132const PENDING_SYNC_PAYLOAD_MAGIC: &[u8; 4] = b"ISB1";
1133const MAX_PENDING_SYNC_ITEMS: usize = 100_000;
1134const MAX_PENDING_SYNC_ITEM_BYTES: usize = 64 * 1024 * 1024;
1135
1136fn encode_pending_payload(
1137 updates: &[proto::Update],
1138 sidecars: Option<&proto::UpdateSidecars>,
1139) -> BackendResult<Vec<u8>> {
1140 if updates.len() > MAX_PENDING_SYNC_ITEMS {
1141 return Err(BackendError::new(
1142 ClientErrorCategory::Internal,
1143 "sync batch is too large to journal",
1144 ));
1145 }
1146 let mut payload = Vec::new();
1147 payload.extend_from_slice(PENDING_SYNC_PAYLOAD_MAGIC);
1148 append_u32(&mut payload, updates.len())?;
1149 for update in updates {
1150 let encoded = update.encode_to_vec();
1151 append_bytes(&mut payload, &encoded)?;
1152 }
1153 match sidecars {
1154 Some(sidecars) => {
1155 payload.push(1);
1156 append_bytes(&mut payload, &sidecars.encode_to_vec())?;
1157 }
1158 None => payload.push(0),
1159 }
1160 Ok(payload)
1161}
1162
1163fn decode_pending_payload(
1164 payload: &[u8],
1165) -> BackendResult<(Vec<proto::Update>, Option<proto::UpdateSidecars>)> {
1166 if !payload.starts_with(PENDING_SYNC_PAYLOAD_MAGIC) {
1167 return Err(invalid_pending_payload("unsupported journal version"));
1168 }
1169 let mut offset = PENDING_SYNC_PAYLOAD_MAGIC.len();
1170 let count = read_u32(payload, &mut offset)?;
1171 if count > MAX_PENDING_SYNC_ITEMS {
1172 return Err(invalid_pending_payload("update count exceeds safety limit"));
1173 }
1174 let mut updates = Vec::with_capacity(count);
1175 for _ in 0..count {
1176 let bytes = read_bytes(payload, &mut offset)?;
1177 updates.push(
1178 proto::Update::decode(bytes)
1179 .map_err(|_| invalid_pending_payload("update protobuf is invalid"))?,
1180 );
1181 }
1182 let sidecars = match payload.get(offset).copied() {
1183 Some(0) => {
1184 offset += 1;
1185 None
1186 }
1187 Some(1) => {
1188 offset += 1;
1189 let bytes = read_bytes(payload, &mut offset)?;
1190 Some(
1191 proto::UpdateSidecars::decode(bytes)
1192 .map_err(|_| invalid_pending_payload("sidecars protobuf is invalid"))?,
1193 )
1194 }
1195 _ => return Err(invalid_pending_payload("sidecars flag is invalid")),
1196 };
1197 if offset != payload.len() {
1198 return Err(invalid_pending_payload("journal has trailing bytes"));
1199 }
1200 Ok((updates, sidecars))
1201}
1202
1203fn append_u32(payload: &mut Vec<u8>, value: usize) -> BackendResult<()> {
1204 let value = u32::try_from(value).map_err(|_| {
1205 BackendError::new(
1206 ClientErrorCategory::Internal,
1207 "sync journal length exceeds u32",
1208 )
1209 })?;
1210 payload.extend_from_slice(&value.to_le_bytes());
1211 Ok(())
1212}
1213
1214fn append_bytes(payload: &mut Vec<u8>, bytes: &[u8]) -> BackendResult<()> {
1215 if bytes.len() > MAX_PENDING_SYNC_ITEM_BYTES {
1216 return Err(BackendError::new(
1217 ClientErrorCategory::Internal,
1218 "sync journal item exceeds safety limit",
1219 ));
1220 }
1221 append_u32(payload, bytes.len())?;
1222 payload.extend_from_slice(bytes);
1223 Ok(())
1224}
1225
1226fn read_u32(payload: &[u8], offset: &mut usize) -> BackendResult<usize> {
1227 let end = offset.saturating_add(4);
1228 let bytes = payload
1229 .get(*offset..end)
1230 .ok_or_else(|| invalid_pending_payload("journal is truncated"))?;
1231 *offset = end;
1232 Ok(u32::from_le_bytes(
1233 bytes
1234 .try_into()
1235 .map_err(|_| invalid_pending_payload("length is invalid"))?,
1236 ) as usize)
1237}
1238
1239fn read_bytes<'a>(payload: &'a [u8], offset: &mut usize) -> BackendResult<&'a [u8]> {
1240 let length = read_u32(payload, offset)?;
1241 if length > MAX_PENDING_SYNC_ITEM_BYTES {
1242 return Err(invalid_pending_payload("journal item exceeds safety limit"));
1243 }
1244 let end = offset.saturating_add(length);
1245 let bytes = payload
1246 .get(*offset..end)
1247 .ok_or_else(|| invalid_pending_payload("journal item is truncated"))?;
1248 *offset = end;
1249 Ok(bytes)
1250}
1251
1252fn invalid_pending_payload(message: &'static str) -> BackendError {
1253 BackendError::new(
1254 ClientErrorCategory::ProtocolMismatch,
1255 format!("invalid pending sync journal: {message}"),
1256 )
1257}
1258
1259#[cfg(test)]
1260mod tests {
1261 use std::{
1262 collections::VecDeque,
1263 sync::{
1264 Arc,
1265 atomic::{AtomicBool, AtomicUsize, Ordering},
1266 },
1267 };
1268
1269 use super::*;
1270 use crate::InMemoryStore;
1271
1272 #[derive(Clone, Debug)]
1273 struct FakeHost {
1274 state: proto::GetUpdatesStateResult,
1275 responses: Arc<Mutex<VecDeque<BackendResult<proto::GetUpdatesResult>>>>,
1276 requests: Arc<Mutex<Vec<proto::GetUpdatesInput>>>,
1277 applied: Arc<Mutex<Vec<Vec<proto::Update>>>>,
1278 repaired_buckets: Arc<Mutex<Vec<SyncBucketKey>>>,
1279 fail_apply: Arc<AtomicBool>,
1280 fetch_delay_ms: u64,
1281 fetches_in_flight: Arc<AtomicUsize>,
1282 max_fetches_in_flight: Arc<AtomicUsize>,
1283 }
1284
1285 impl FakeHost {
1286 fn new(responses: Vec<proto::GetUpdatesResult>) -> Self {
1287 Self {
1288 state: proto::GetUpdatesStateResult {
1289 date: 100,
1290 updates_found: Some(true),
1291 core_sync_schema_revision: crate::CORE_SYNC_SCHEMA_REVISION,
1292 },
1293 responses: Arc::new(Mutex::new(
1294 responses.into_iter().map(Ok).collect::<VecDeque<_>>(),
1295 )),
1296 requests: Arc::new(Mutex::new(Vec::new())),
1297 applied: Arc::new(Mutex::new(Vec::new())),
1298 repaired_buckets: Arc::new(Mutex::new(Vec::new())),
1299 fail_apply: Arc::new(AtomicBool::new(false)),
1300 fetch_delay_ms: 0,
1301 fetches_in_flight: Arc::new(AtomicUsize::new(0)),
1302 max_fetches_in_flight: Arc::new(AtomicUsize::new(0)),
1303 }
1304 }
1305
1306 fn failing_apply(mut self) -> Self {
1307 self.fail_apply = Arc::new(AtomicBool::new(true));
1308 self
1309 }
1310
1311 fn with_fetch_delay(mut self, delay_ms: u64) -> Self {
1312 self.fetch_delay_ms = delay_ms;
1313 self
1314 }
1315 }
1316
1317 impl SyncHost for FakeHost {
1318 fn get_updates_state(
1319 &self,
1320 _date: i64,
1321 ) -> BoxFuture<'static, BackendResult<proto::GetUpdatesStateResult>> {
1322 let state = self.state.clone();
1323 Box::pin(async move { Ok(state) })
1324 }
1325
1326 fn get_updates(
1327 &self,
1328 input: proto::GetUpdatesInput,
1329 ) -> BoxFuture<'static, BackendResult<proto::GetUpdatesResult>> {
1330 let host = self.clone();
1331 Box::pin(async move {
1332 let in_flight = host.fetches_in_flight.fetch_add(1, Ordering::SeqCst) + 1;
1333 host.max_fetches_in_flight
1334 .fetch_max(in_flight, Ordering::SeqCst);
1335 if host.fetch_delay_ms > 0 {
1336 tokio::time::sleep(std::time::Duration::from_millis(host.fetch_delay_ms)).await;
1337 }
1338 host.requests.lock().await.push(input);
1339 let response = host.responses.lock().await.pop_front().unwrap_or_else(|| {
1340 Err(BackendError::new(
1341 ClientErrorCategory::Internal,
1342 "missing fake getUpdates response",
1343 ))
1344 });
1345 host.fetches_in_flight.fetch_sub(1, Ordering::SeqCst);
1346 response
1347 })
1348 }
1349
1350 fn apply_sync_batch(
1351 &self,
1352 updates: Vec<proto::Update>,
1353 _sidecars: Option<proto::UpdateSidecars>,
1354 ) -> BoxFuture<'static, BackendResult<Vec<ClientEvent>>> {
1355 let host = self.clone();
1356 Box::pin(async move {
1357 if host.fail_apply.load(Ordering::Relaxed) {
1358 return Err(BackendError::new(
1359 ClientErrorCategory::Internal,
1360 "fake apply failure",
1361 ));
1362 }
1363 host.applied.lock().await.push(updates.clone());
1364 Ok(updates
1365 .iter()
1366 .filter_map(|update| match update.update.as_ref() {
1367 Some(proto::update::Update::NewMessage(update)) => {
1368 let message = update.message.as_ref()?;
1369 Some(ClientEvent::MessageUpserted {
1370 chat_id: InlineId::new(message.chat_id),
1371 message_id: InlineId::new(message.id),
1372 })
1373 }
1374 _ => None,
1375 })
1376 .collect())
1377 })
1378 }
1379
1380 fn repair_bucket(
1381 &self,
1382 key: SyncBucketKey,
1383 ) -> BoxFuture<'static, BackendResult<Vec<ClientEvent>>> {
1384 let host = self.clone();
1385 Box::pin(async move {
1386 host.repaired_buckets.lock().await.push(key);
1387 Ok(Vec::new())
1388 })
1389 }
1390 }
1391
1392 #[tokio::test]
1393 async fn hint_only_recovery_pages_to_target_and_persists_cursor() {
1394 let store = Arc::new(InMemoryStore::new());
1395 let key = chat_key(7);
1396 store
1397 .save_sync_bucket_state(key, SyncBucketState { seq: 1, date: 10 })
1398 .await
1399 .unwrap();
1400 let host = FakeHost::new(vec![
1401 updates_result(vec![message_update(2, 20, 7, 102)], 2, 20, false),
1402 updates_result(vec![message_update(3, 30, 7, 103)], 3, 30, true),
1403 ]);
1404 let sync = SyncManager::new(store.clone(), SyncConfig::default());
1405
1406 let events = sync
1407 .process_realtime(&host, vec![chat_hint(7, 3)])
1408 .await
1409 .unwrap();
1410
1411 assert_eq!(events.len(), 2);
1412 assert_eq!(
1413 store.sync_bucket_state(key).await.unwrap(),
1414 SyncBucketState { seq: 3, date: 30 }
1415 );
1416 let requests = host.requests.lock().await;
1417 assert_eq!(requests.len(), 2);
1418 assert_eq!(requests[0].start_seq, 1);
1419 assert_eq!(requests[0].seq_end, 3);
1420 assert_eq!(requests[1].start_seq, 2);
1421 assert_eq!(requests[1].seq_end, 3);
1422 }
1423
1424 #[tokio::test]
1425 async fn safe_page_accepts_future_revision_metadata() {
1426 let store = Arc::new(InMemoryStore::new());
1427 let key = chat_key(7);
1428 store
1429 .save_sync_bucket_state(key, SyncBucketState { seq: 1, date: 10 })
1430 .await
1431 .unwrap();
1432 let mut response = updates_result(vec![message_update(2, 20, 7, 102)], 2, 20, true);
1433 response.core_sync_schema_revision = crate::CORE_SYNC_SCHEMA_REVISION + 1;
1434 let host = FakeHost::new(vec![response]);
1435 let sync = SyncManager::new(store.clone(), SyncConfig::default());
1436
1437 let events = sync
1438 .process_realtime(&host, vec![chat_hint(7, 2)])
1439 .await
1440 .unwrap();
1441
1442 assert_eq!(events.len(), 1);
1443 assert_eq!(
1444 store.sync_bucket_state(key).await.unwrap(),
1445 SyncBucketState { seq: 2, date: 20 }
1446 );
1447 }
1448
1449 #[tokio::test]
1450 async fn discovery_accepts_unversioned_state_and_page_metadata() {
1451 let store = Arc::new(InMemoryStore::new());
1452 let mut response = skipped_result(0, 1, 20);
1453 response.core_sync_schema_revision = 0;
1454 let mut host = FakeHost::new(vec![response]);
1455 host.state.core_sync_schema_revision = 0;
1456 let sync = SyncManager::new(store.clone(), SyncConfig::default());
1457
1458 let events = sync.discover(&host).await.unwrap();
1459
1460 assert!(events.is_empty());
1461 assert_eq!(
1462 store.sync_bucket_state(SyncBucketKey::User).await.unwrap(),
1463 SyncBucketState { seq: 1, date: 20 }
1464 );
1465 }
1466
1467 #[tokio::test]
1468 async fn warm_catchup_commits_progress_across_more_than_128_pages() {
1469 let store = Arc::new(InMemoryStore::new());
1470 let key = chat_key(7);
1471 store
1472 .save_sync_bucket_state(key, SyncBucketState { seq: 1, date: 10 })
1473 .await
1474 .unwrap();
1475 let responses = (2..=131)
1476 .map(|seq| {
1477 updates_result(
1478 vec![message_update(seq, i64::from(seq), 7, i64::from(seq) + 100)],
1479 i64::from(seq),
1480 i64::from(seq),
1481 seq == 131,
1482 )
1483 })
1484 .collect::<Vec<_>>();
1485 let host = FakeHost::new(responses);
1486 let sync = SyncManager::new(store.clone(), SyncConfig::default());
1487
1488 let events = sync
1489 .process_realtime(&host, vec![chat_hint(7, 131)])
1490 .await
1491 .unwrap();
1492
1493 assert_eq!(events.len(), 130);
1494 assert_eq!(host.requests.lock().await.len(), 130);
1495 assert_eq!(host.applied.lock().await.len(), 130);
1496 assert_eq!(store.sync_bucket_state(key).await.unwrap().seq, 131);
1497 assert!(store.pending_sync_batches().await.unwrap().is_empty());
1498 }
1499
1500 #[tokio::test]
1501 async fn gap_recovery_merges_buffered_realtime_update_before_commit() {
1502 let store = Arc::new(InMemoryStore::new());
1503 let key = chat_key(7);
1504 store
1505 .save_sync_bucket_state(key, SyncBucketState { seq: 1, date: 10 })
1506 .await
1507 .unwrap();
1508 let host = FakeHost::new(vec![
1509 updates_result(vec![message_update(2, 20, 7, 102)], 2, 20, false),
1510 updates_result(vec![message_update(3, 30, 7, 103)], 3, 30, true),
1511 ]);
1512 let sync = SyncManager::new(store.clone(), SyncConfig::default());
1513
1514 sync.process_realtime(&host, vec![message_update(3, 30, 7, 103)])
1515 .await
1516 .unwrap();
1517
1518 let applied = host.applied.lock().await;
1519 assert_eq!(applied.len(), 2);
1520 assert_eq!(
1521 applied
1522 .iter()
1523 .flat_map(|batch| batch.iter().map(update_seq))
1524 .collect::<Vec<_>>(),
1525 vec![2, 3]
1526 );
1527 assert_eq!(
1528 store.sync_bucket_state(key).await.unwrap(),
1529 SyncBucketState { seq: 3, date: 30 }
1530 );
1531 }
1532
1533 #[tokio::test]
1534 async fn apply_failure_does_not_advance_bucket_cursor() {
1535 let store = Arc::new(InMemoryStore::new());
1536 let key = chat_key(7);
1537 store
1538 .save_sync_bucket_state(key, SyncBucketState { seq: 1, date: 10 })
1539 .await
1540 .unwrap();
1541 let host = FakeHost::new(vec![updates_result(
1542 vec![message_update(2, 20, 7, 101)],
1543 2,
1544 20,
1545 true,
1546 )])
1547 .failing_apply();
1548 let sync = SyncManager::new(store.clone(), SyncConfig::default());
1549
1550 let error = sync
1551 .process_realtime(&host, vec![chat_hint(7, 2)])
1552 .await
1553 .unwrap_err();
1554
1555 assert_eq!(error.category, ClientErrorCategory::Internal);
1556 assert_eq!(
1557 store.sync_bucket_state(key).await.unwrap(),
1558 SyncBucketState { seq: 1, date: 10 }
1559 );
1560 assert_eq!(store.pending_sync_batches().await.unwrap().len(), 1);
1561
1562 host.fail_apply.store(false, Ordering::Relaxed);
1563 let recovered = sync.process_realtime(&host, Vec::new()).await.unwrap();
1564 assert_eq!(recovered.len(), 1);
1565 assert_eq!(
1566 store.sync_bucket_state(key).await.unwrap(),
1567 SyncBucketState { seq: 2, date: 20 }
1568 );
1569 assert!(store.pending_sync_batches().await.unwrap().is_empty());
1570 }
1571
1572 #[test]
1573 fn pending_sync_payload_round_trips_updates_and_sidecars() {
1574 let updates = vec![message_update(2, 20, 7, 102)];
1575 let sidecars = proto::UpdateSidecars {
1576 users: vec![proto::User {
1577 id: 42,
1578 ..Default::default()
1579 }],
1580 ..Default::default()
1581 };
1582 let payload = encode_pending_payload(&updates, Some(&sidecars)).unwrap();
1583
1584 let (decoded_updates, decoded_sidecars) = decode_pending_payload(&payload).unwrap();
1585 assert_eq!(decoded_updates, updates);
1586 assert_eq!(decoded_sidecars, Some(sidecars));
1587 }
1588
1589 #[tokio::test]
1590 async fn incompatible_pending_journal_is_discarded_and_refetched_without_advancing_first() {
1591 let store = Arc::new(InMemoryStore::new());
1592 let key = chat_key(7);
1593 store
1594 .save_sync_bucket_state(key, SyncBucketState { seq: 1, date: 10 })
1595 .await
1596 .unwrap();
1597 let invalid = proto::Update {
1598 seq: Some(2),
1599 date: Some(20),
1600 update: None,
1601 };
1602 store
1603 .save_pending_sync_batch(PendingSyncBatch {
1604 key,
1605 committed_state: SyncBucketState { seq: 2, date: 20 },
1606 payload: encode_pending_payload(&[invalid], None).unwrap(),
1607 })
1608 .await
1609 .unwrap();
1610 let host = FakeHost::new(vec![updates_result(
1611 vec![message_update(2, 20, 7, 102)],
1612 2,
1613 20,
1614 true,
1615 )]);
1616 let sync = SyncManager::new(store.clone(), SyncConfig::default());
1617
1618 let events = sync.process_realtime(&host, Vec::new()).await.unwrap();
1619
1620 assert_eq!(events.len(), 1);
1621 assert_eq!(host.requests.lock().await.len(), 1);
1622 assert_eq!(
1623 store.sync_bucket_state(key).await.unwrap(),
1624 SyncBucketState { seq: 2, date: 20 }
1625 );
1626 assert!(store.pending_sync_batches().await.unwrap().is_empty());
1627 }
1628
1629 #[test]
1630 fn page_envelope_rejects_missing_duplicate_and_out_of_range_sequences() {
1631 let missing = updates_result(vec![message_update(2, 20, 7, 102)], 3, 30, true);
1632 assert!(validate_response_page(1, &missing).is_err());
1633
1634 let duplicate = updates_result(
1635 vec![message_update(2, 20, 7, 102), message_update(2, 20, 7, 102)],
1636 2,
1637 20,
1638 true,
1639 );
1640 assert!(validate_response_page(1, &duplicate).is_err());
1641
1642 let out_of_range = updates_result(vec![message_update(4, 40, 7, 104)], 3, 40, true);
1643 assert!(validate_response_page(1, &out_of_range).is_err());
1644 }
1645
1646 #[test]
1647 fn page_envelope_accepts_explicit_irrelevant_sequence_accounting() {
1648 let mut response = updates_result(vec![message_update(3, 30, 7, 103)], 3, 30, true);
1649 response.skipped_sequences = vec![proto::SyncSkippedSequence {
1650 seq: 2,
1651 reason: proto::sync_skipped_sequence::Reason::IrrelevantToBucket as i32,
1652 }];
1653
1654 assert_eq!(
1655 validate_response_page(1, &response).unwrap(),
1656 PageAccounting::default()
1657 );
1658 }
1659
1660 #[tokio::test]
1661 async fn empty_result_behind_hint_is_retried_without_advancing() {
1662 let store = Arc::new(InMemoryStore::new());
1663 let key = chat_key(7);
1664 store
1665 .save_sync_bucket_state(key, SyncBucketState { seq: 5, date: 50 })
1666 .await
1667 .unwrap();
1668 let empty = || proto::GetUpdatesResult {
1669 updates: Vec::new(),
1670 seq: 5,
1671 date: 80,
1672 r#final: Some(true),
1673 result_type: proto::get_updates_result::ResultType::Empty as i32,
1674 core_sync_schema_revision: crate::CORE_SYNC_SCHEMA_REVISION,
1675 ..Default::default()
1676 };
1677 let host = FakeHost::new(vec![empty(), empty(), empty()]);
1678 let sync = SyncManager::new(
1679 store.clone(),
1680 SyncConfig {
1681 inconsistent_empty_retry_attempts: 2,
1682 inconsistent_empty_retry_delay_ms: 0,
1683 ..SyncConfig::default()
1684 },
1685 );
1686
1687 let error = sync
1688 .process_realtime(&host, vec![chat_hint(7, 8)])
1689 .await
1690 .unwrap_err();
1691
1692 assert_eq!(error.category, ClientErrorCategory::ProtocolMismatch);
1693 assert_eq!(
1694 store.sync_bucket_state(key).await.unwrap(),
1695 SyncBucketState { seq: 5, date: 50 }
1696 );
1697 }
1698
1699 #[tokio::test]
1700 async fn cold_chat_too_long_repairs_snapshot_and_advances_to_hint() {
1701 let store = Arc::new(InMemoryStore::new());
1702 let key = chat_key(7);
1703 let host = FakeHost::new(vec![
1704 proto::GetUpdatesResult {
1705 updates: Vec::new(),
1706 seq: 400,
1707 date: 80,
1708 r#final: Some(false),
1709 result_type: proto::get_updates_result::ResultType::TooLong as i32,
1710 core_sync_schema_revision: crate::CORE_SYNC_SCHEMA_REVISION,
1711 ..Default::default()
1712 },
1713 skipped_result(400, 500, 90),
1714 ]);
1715 let sync = SyncManager::new(store.clone(), SyncConfig::default());
1716
1717 sync.process_realtime(&host, vec![chat_hint(7, 500)])
1718 .await
1719 .unwrap();
1720
1721 assert_eq!(host.repaired_buckets.lock().await.as_slice(), &[key]);
1722 assert_eq!(
1723 store.sync_bucket_state(key).await.unwrap(),
1724 SyncBucketState { seq: 500, date: 90 }
1725 );
1726 assert_eq!(host.requests.lock().await.len(), 2);
1727 }
1728
1729 #[tokio::test]
1730 async fn cold_pointer_commits_lossless_events_without_an_empty_journal() {
1731 let store = Arc::new(InMemoryStore::new());
1732 let key = chat_key(7);
1733 let sync = SyncManager::new(store.clone(), SyncConfig::default());
1734 let event = ClientEvent::MessageDeleted {
1735 chat_id: InlineId::new(7),
1736 message_id: InlineId::new(101),
1737 };
1738
1739 let deliveries = sync
1740 .commit_cold_pointer(key, 500, 80, vec![event.clone()])
1741 .await
1742 .unwrap();
1743
1744 assert_eq!(deliveries.len(), 1);
1745 assert_eq!(deliveries[0].event, event);
1746 assert!(deliveries[0].delivery_id.is_some());
1747 assert!(store.pending_sync_batches().await.unwrap().is_empty());
1748 assert_eq!(
1749 store.sync_bucket_state(key).await.unwrap(),
1750 SyncBucketState { seq: 500, date: 80 }
1751 );
1752 assert_eq!(store.pending_client_events().await.unwrap(), deliveries);
1753 }
1754
1755 #[tokio::test]
1756 async fn cold_user_and_space_too_long_require_snapshots_before_advancing() {
1757 for key in [
1758 SyncBucketKey::User,
1759 SyncBucketKey::Space {
1760 space_id: InlineId::new(9),
1761 },
1762 ] {
1763 let store = Arc::new(InMemoryStore::new());
1764 let host = FakeHost::new(vec![
1765 proto::GetUpdatesResult {
1766 updates: Vec::new(),
1767 seq: 400,
1768 date: 80,
1769 r#final: Some(false),
1770 result_type: proto::get_updates_result::ResultType::TooLong as i32,
1771 core_sync_schema_revision: crate::CORE_SYNC_SCHEMA_REVISION,
1772 ..Default::default()
1773 },
1774 skipped_result(400, 500, 90),
1775 ]);
1776 let sync = SyncManager::new(store.clone(), SyncConfig::default());
1777
1778 sync.fetch_bucket(
1779 &host,
1780 key,
1781 SyncBucketState::default(),
1782 BTreeMap::new(),
1783 Some(500),
1784 )
1785 .await
1786 .unwrap();
1787
1788 assert_eq!(host.repaired_buckets.lock().await.as_slice(), &[key]);
1789 assert_eq!(
1790 store.sync_bucket_state(key).await.unwrap(),
1791 SyncBucketState { seq: 500, date: 90 }
1792 );
1793 }
1794 }
1795
1796 #[tokio::test]
1797 async fn independent_bucket_fetches_use_configured_concurrency() {
1798 let store = Arc::new(InMemoryStore::new());
1799 let empty = |date| proto::GetUpdatesResult {
1800 updates: Vec::new(),
1801 seq: 1,
1802 date,
1803 r#final: Some(true),
1804 result_type: proto::get_updates_result::ResultType::Empty as i32,
1805 skipped_sequences: vec![proto::SyncSkippedSequence {
1806 seq: 1,
1807 reason: proto::sync_skipped_sequence::Reason::IrrelevantToBucket as i32,
1808 }],
1809 core_sync_schema_revision: crate::CORE_SYNC_SCHEMA_REVISION,
1810 ..Default::default()
1811 };
1812 let host = FakeHost::new(vec![empty(100), empty(200)]).with_fetch_delay(25);
1813 let sync = SyncManager::new(
1814 store.clone(),
1815 SyncConfig {
1816 max_concurrent_bucket_fetches: 2,
1817 ..SyncConfig::default()
1818 },
1819 );
1820
1821 sync.process_realtime(&host, vec![chat_hint(7, 1), chat_hint(8, 1)])
1822 .await
1823 .unwrap();
1824
1825 assert_eq!(host.max_fetches_in_flight.load(Ordering::SeqCst), 2);
1826 assert_eq!(store.sync_bucket_state(chat_key(7)).await.unwrap().seq, 1);
1827 assert_eq!(store.sync_bucket_state(chat_key(8)).await.unwrap().seq, 1);
1828 assert_eq!(store.sync_state().await.unwrap().last_sync_date, 185);
1829 }
1830
1831 fn updates_result(
1832 updates: Vec<proto::Update>,
1833 seq: i64,
1834 date: i64,
1835 final_page: bool,
1836 ) -> proto::GetUpdatesResult {
1837 proto::GetUpdatesResult {
1838 updates,
1839 seq,
1840 date,
1841 r#final: Some(final_page),
1842 result_type: proto::get_updates_result::ResultType::Slice as i32,
1843 core_sync_schema_revision: crate::CORE_SYNC_SCHEMA_REVISION,
1844 ..Default::default()
1845 }
1846 }
1847
1848 fn skipped_result(start_seq: i64, end_seq: i64, date: i64) -> proto::GetUpdatesResult {
1849 proto::GetUpdatesResult {
1850 updates: Vec::new(),
1851 seq: end_seq,
1852 date,
1853 r#final: Some(true),
1854 result_type: proto::get_updates_result::ResultType::Empty as i32,
1855 skipped_sequences: ((start_seq + 1)..=end_seq)
1856 .map(|seq| proto::SyncSkippedSequence {
1857 seq,
1858 reason: proto::sync_skipped_sequence::Reason::IrrelevantToBucket as i32,
1859 })
1860 .collect(),
1861 core_sync_schema_revision: crate::CORE_SYNC_SCHEMA_REVISION,
1862 ..Default::default()
1863 }
1864 }
1865
1866 fn chat_hint(chat_id: i64, seq: i32) -> proto::Update {
1867 proto::Update {
1868 seq: None,
1869 date: None,
1870 update: Some(proto::update::Update::ChatHasNewUpdates(
1871 proto::UpdateChatHasNewUpdates {
1872 chat_id,
1873 update_seq: seq,
1874 peer_id: Some(chat_peer(chat_id)),
1875 },
1876 )),
1877 }
1878 }
1879
1880 fn message_update(seq: i32, date: i64, chat_id: i64, message_id: i64) -> proto::Update {
1881 proto::Update {
1882 seq: Some(seq),
1883 date: Some(date),
1884 update: Some(proto::update::Update::NewMessage(proto::UpdateNewMessage {
1885 message: Some(proto::Message {
1886 id: message_id,
1887 chat_id,
1888 peer_id: Some(chat_peer(chat_id)),
1889 ..Default::default()
1890 }),
1891 })),
1892 }
1893 }
1894
1895 fn chat_peer(chat_id: i64) -> proto::Peer {
1896 proto::Peer {
1897 r#type: Some(proto::peer::Type::Chat(proto::PeerChat { chat_id })),
1898 }
1899 }
1900}