1use std::{
4 collections::{BTreeMap, HashMap},
5 sync::Arc,
6 time::{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, ClientStore, InlineId,
16 PendingSyncBatch, StoreError, SyncBucketKey, SyncBucketPeer, SyncBucketState, SyncState,
17};
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub struct SyncConfig {
22 pub last_sync_safety_gap_seconds: i64,
24 pub max_concurrent_bucket_fetches: usize,
26 pub initial_lookback_seconds: i64,
28 pub stale_state_max_age_seconds: i64,
30 pub max_total_updates: i32,
32 pub page_limit: i32,
34 pub cold_start_total_limit: i32,
36}
37
38impl Default for SyncConfig {
39 fn default() -> Self {
40 Self {
41 last_sync_safety_gap_seconds: 15,
42 max_concurrent_bucket_fetches: 4,
43 initial_lookback_seconds: 5 * 24 * 60 * 60,
44 stale_state_max_age_seconds: 14 * 24 * 60 * 60,
45 max_total_updates: 1_000,
46 page_limit: 200,
47 cold_start_total_limit: 50,
48 }
49 }
50}
51
52pub(crate) trait SyncHost: Clone + Send + Sync + 'static {
53 fn get_updates_state(
54 &self,
55 date: i64,
56 ) -> BoxFuture<'static, BackendResult<proto::GetUpdatesStateResult>>;
57
58 fn get_updates(
59 &self,
60 input: proto::GetUpdatesInput,
61 ) -> BoxFuture<'static, BackendResult<proto::GetUpdatesResult>>;
62
63 fn apply_sync_batch(
64 &self,
65 updates: Vec<proto::Update>,
66 sidecars: Option<proto::UpdateSidecars>,
67 ) -> BoxFuture<'static, BackendResult<Vec<ClientEvent>>>;
68
69 fn repair_bucket(
72 &self,
73 key: SyncBucketKey,
74 ) -> BoxFuture<'static, BackendResult<Vec<ClientEvent>>>;
75}
76
77#[derive(Debug)]
78pub(crate) struct SyncManager {
79 store: Arc<dyn ClientStore>,
80 config: SyncConfig,
81 bucket_locks: Mutex<HashMap<SyncBucketKey, Arc<Mutex<()>>>>,
82 fetch_limiter: Arc<Semaphore>,
83 sync_state_lock: Mutex<()>,
84}
85
86impl SyncManager {
87 pub(crate) fn new(store: Arc<dyn ClientStore>, config: SyncConfig) -> Self {
88 let max_fetches = config.max_concurrent_bucket_fetches.max(1);
89 Self {
90 store,
91 config,
92 bucket_locks: Mutex::new(HashMap::new()),
93 fetch_limiter: Arc::new(Semaphore::new(max_fetches)),
94 sync_state_lock: Mutex::new(()),
95 }
96 }
97
98 pub(crate) async fn discover<H: SyncHost>(&self, host: &H) -> BackendResult<Vec<ClientEvent>> {
99 let mut events = self.recover_pending_batches(host).await?;
100 let state = self.prepared_sync_state().await?;
101 log::debug!(
102 "starting Inline update discovery from date={}",
103 state.last_sync_date
104 );
105 let result = host.get_updates_state(state.last_sync_date).await?;
106 if result.updates_found == Some(false) {
107 self.update_last_sync_date(result.date).await?;
108 }
109 events.extend(
110 self.process_bucket(host, SyncBucketKey::User, Vec::new(), None, true)
111 .await?,
112 );
113 log::debug!(
114 "finished Inline update discovery date={} events={}",
115 result.date,
116 events.len()
117 );
118 Ok(events)
119 }
120
121 pub(crate) async fn process_realtime<H: SyncHost>(
122 &self,
123 host: &H,
124 updates: Vec<proto::Update>,
125 ) -> BackendResult<Vec<ClientEvent>> {
126 let mut events = self.recover_pending_batches(host).await?;
127 let received_count = updates.len();
128 let mut direct = Vec::new();
129 let mut buckets = HashMap::<SyncBucketKey, Vec<proto::Update>>::new();
130 let mut targets = HashMap::<SyncBucketKey, i64>::new();
131
132 for update in updates {
133 match update.update.as_ref() {
134 Some(proto::update::Update::ChatHasNewUpdates(hint)) => {
135 if let Some(key) = chat_hint_bucket_key(hint) {
136 targets
137 .entry(key)
138 .and_modify(|seq| *seq = (*seq).max(i64::from(hint.update_seq)))
139 .or_insert(i64::from(hint.update_seq));
140 }
141 }
142 Some(proto::update::Update::SpaceHasNewUpdates(hint)) => {
143 let key = SyncBucketKey::Space {
144 space_id: InlineId::new(hint.space_id),
145 };
146 targets
147 .entry(key)
148 .and_modify(|seq| *seq = (*seq).max(i64::from(hint.update_seq)))
149 .or_insert(i64::from(hint.update_seq));
150 }
151 _ => {
152 let seq = update_seq(&update);
153 if seq > 0
154 && let Some(key) = bucket_key_for_update(&update)
155 {
156 buckets.entry(key).or_default().push(update);
157 } else {
158 direct.push(update);
159 }
160 }
161 }
162 }
163
164 if !direct.is_empty() {
165 let max_date = max_update_date(&direct);
166 let applied = host.apply_sync_batch(direct, None).await?;
167 self.update_last_sync_date(max_date).await?;
168 events.extend(applied);
169 }
170
171 for key in targets.keys() {
172 buckets.entry(*key).or_default();
173 }
174
175 let mut ordered = buckets
176 .into_iter()
177 .map(|(key, updates)| {
178 let target = targets.get(&key).copied();
179 (key, updates, target)
180 })
181 .collect::<Vec<_>>();
182 let bucket_count = ordered.len();
183 ordered.sort_by_key(|(key, _, _)| bucket_sort_key(*key));
184
185 if let Some(user_index) = ordered
186 .iter()
187 .position(|(key, _, _)| *key == SyncBucketKey::User)
188 {
189 let (key, updates, target) = ordered.remove(user_index);
190 events.extend(
191 self.process_bucket(host, key, updates, target, false)
192 .await?,
193 );
194 }
195
196 let concurrency = self.config.max_concurrent_bucket_fetches.max(1);
197 let mut results = stream::iter(ordered)
198 .map(|(key, updates, target)| async move {
199 (
200 bucket_sort_key(key),
201 self.process_bucket(host, key, updates, target, false).await,
202 )
203 })
204 .buffer_unordered(concurrency)
205 .collect::<Vec<_>>()
206 .await;
207 results.sort_by_key(|(sort_key, _)| *sort_key);
208 for (_, result) in results {
209 events.extend(result?);
210 }
211 log::debug!(
212 "processed Inline realtime batch received={received_count} buckets={bucket_count} events={}",
213 events.len()
214 );
215 Ok(events)
216 }
217
218 async fn process_bucket<H: SyncHost>(
219 &self,
220 host: &H,
221 key: SyncBucketKey,
222 realtime_updates: Vec<proto::Update>,
223 target_seq: Option<i64>,
224 force_fetch: bool,
225 ) -> BackendResult<Vec<ClientEvent>> {
226 let bucket_lock = self.bucket_lock(key).await;
227 let _bucket_guard = bucket_lock.lock().await;
228 let mut state = self
229 .store
230 .sync_bucket_state(key)
231 .await
232 .map_err(store_error_to_backend)?;
233 let mut buffered = BTreeMap::new();
234 for update in realtime_updates {
235 let seq = update_seq(&update);
236 if seq > state.seq {
237 buffered.insert(seq, update);
238 }
239 }
240
241 let mut events = self
242 .drain_contiguous(host, key, &mut state, &mut buffered)
243 .await?;
244 let target_seq = target_seq
245 .filter(|target| *target > state.seq)
246 .or_else(|| buffered.last_key_value().map(|(seq, _)| *seq));
247 if !force_fetch && target_seq.is_none() && buffered.is_empty() && state.seq > 0 {
248 return Ok(events);
249 }
250
251 events.extend(
252 self.fetch_bucket(host, key, state, buffered, target_seq)
253 .await?,
254 );
255 Ok(events)
256 }
257
258 async fn drain_contiguous<H: SyncHost>(
259 &self,
260 host: &H,
261 key: SyncBucketKey,
262 state: &mut SyncBucketState,
263 buffered: &mut BTreeMap<i64, proto::Update>,
264 ) -> BackendResult<Vec<ClientEvent>> {
265 let mut updates = Vec::new();
266 let mut next_seq = state.seq + 1;
267 while let Some(update) = buffered.remove(&next_seq) {
268 updates.push(update);
269 next_seq += 1;
270 }
271 if updates.is_empty() {
272 return Ok(Vec::new());
273 }
274
275 let committed = SyncBucketState {
276 seq: next_seq - 1,
277 date: state.date.max(max_update_date(&updates)),
278 };
279 let events = self
280 .commit_sync_batch(host, key, committed, updates, None)
281 .await?;
282 self.update_last_sync_date(committed.date).await?;
283 *state = committed;
284 Ok(events)
285 }
286
287 async fn fetch_bucket<H: SyncHost>(
288 &self,
289 host: &H,
290 key: SyncBucketKey,
291 initial: SyncBucketState,
292 buffered: BTreeMap<i64, proto::Update>,
293 target_seq: Option<i64>,
294 ) -> BackendResult<Vec<ClientEvent>> {
295 let _permit = self
296 .fetch_limiter
297 .clone()
298 .acquire_owned()
299 .await
300 .map_err(|_| BackendError::new(ClientErrorCategory::Internal, "sync fetch stopped"))?;
301 let cold_start = initial.seq == 0 || initial.date == 0;
302 let mut current_seq = initial.seq;
303 let mut final_date = initial.date;
304 let hard_end = target_seq.or_else(|| buffered.last_key_value().map(|(seq, _)| *seq));
305 let mut slice_end = None;
306 let mut fetched = BTreeMap::<i64, proto::Update>::new();
307 let mut unsequenced = Vec::new();
308 let mut sidecars = proto::UpdateSidecars::default();
309 let mut page_count = 0_u32;
310
311 log::debug!(
312 "starting Inline bucket fetch kind={} start_seq={} target_seq={:?} cold_start={cold_start}",
313 bucket_kind(key),
314 current_seq,
315 hard_end
316 );
317
318 loop {
319 page_count += 1;
320 if page_count > 128 {
321 return Err(BackendError::new(
322 ClientErrorCategory::ProtocolMismatch,
323 "bucket sync exceeded the page safety limit",
324 ));
325 }
326 let request_end = slice_end.or(hard_end).filter(|end| *end > current_seq);
327 let response = host
328 .get_updates(proto::GetUpdatesInput {
329 bucket: Some(protocol_bucket(key)),
330 start_seq: current_seq,
331 total_limit: if cold_start && slice_end.is_none() {
332 self.config.cold_start_total_limit
333 } else {
334 self.config.max_total_updates
335 },
336 seq_end: request_end.unwrap_or_default(),
337 limit: self.config.page_limit,
338 })
339 .await?;
340 let result_type = proto::get_updates_result::ResultType::try_from(response.result_type)
341 .unwrap_or(proto::get_updates_result::ResultType::Unspecified);
342
343 if result_type == proto::get_updates_result::ResultType::TooLong {
344 if response.seq <= current_seq {
345 return Err(BackendError::new(
346 ClientErrorCategory::ProtocolMismatch,
347 "bucket sync received a non-advancing TOO_LONG pointer",
348 ));
349 }
350 if cold_start {
351 let target_seq = hard_end.unwrap_or(response.seq);
352 let target_date = final_date.max(response.date);
353 log::warn!(
354 "repairing cold Inline bucket after TOO_LONG kind={} target_seq={target_seq}",
355 bucket_kind(key)
356 );
357 return self
358 .repair_cold_bucket(host, key, target_seq, target_date)
359 .await;
360 }
361 let max_slice = current_seq + i64::from(self.config.max_total_updates);
362 slice_end = Some(response.seq.min(max_slice));
363 continue;
364 }
365
366 if response.seq < current_seq {
367 return Err(BackendError::new(
368 ClientErrorCategory::ProtocolMismatch,
369 "bucket sync server cursor moved backwards",
370 ));
371 }
372 let previous_seq = current_seq;
373 current_seq = response.seq;
374 final_date = final_date.max(response.date);
375 if let Some(page_sidecars) = response.sidecars {
376 merge_sidecars(&mut sidecars, page_sidecars);
377 }
378 for update in response.updates {
379 let seq = update_seq(&update);
380 if seq > initial.seq {
381 fetched.insert(seq, update);
382 } else if seq == 0 {
383 unsequenced.push(update);
384 }
385 }
386
387 let empty = result_type == proto::get_updates_result::ResultType::Empty;
388 if empty {
389 current_seq = current_seq.max(hard_end.unwrap_or_default());
390 }
391 let final_page = response.r#final.unwrap_or(false) || empty;
392 if current_seq == previous_seq && !final_page {
393 return Err(BackendError::new(
394 ClientErrorCategory::ProtocolMismatch,
395 "bucket sync received a non-progress response",
396 ));
397 }
398 if let Some(end) = slice_end
399 && current_seq >= end
400 {
401 slice_end = None;
402 if hard_end.is_some_and(|target| current_seq < target) {
403 continue;
404 }
405 }
406 if hard_end.is_some_and(|target| current_seq >= target) || final_page {
407 break;
408 }
409 }
410
411 for (seq, update) in buffered {
412 if seq > initial.seq && seq <= current_seq {
413 fetched.entry(seq).or_insert(update);
414 }
415 }
416 let mut updates = unsequenced;
417 updates.extend(fetched.into_values());
418 let max_delivered_seq = updates.iter().map(update_seq).max().unwrap_or(initial.seq);
419 if current_seq > max_delivered_seq {
420 log::warn!(
421 "trusting Inline bucket pointer ahead of delivered updates kind={} delivered_seq={max_delivered_seq} pointer_seq={current_seq}",
422 bucket_kind(key)
423 );
424 }
425 let committed = SyncBucketState {
426 seq: current_seq,
427 date: final_date.max(max_update_date(&updates)),
428 };
429 if cold_start {
430 let events = self
431 .repair_cold_bucket(host, key, committed.seq, committed.date)
432 .await?;
433 log::debug!(
434 "finished cold Inline bucket snapshot kind={} pages={page_count} end_seq={} events={}",
435 bucket_kind(key),
436 committed.seq,
437 events.len()
438 );
439 return Ok(events);
440 }
441 let has_sidecars = !sidecars.users.is_empty()
442 || !sidecars.chats.is_empty()
443 || !sidecars.dialogs.is_empty()
444 || !sidecars.spaces.is_empty();
445 let update_count = updates.len();
446 let events = self
447 .commit_sync_batch(
448 host,
449 key,
450 committed,
451 updates,
452 has_sidecars.then_some(sidecars),
453 )
454 .await?;
455 self.update_last_sync_date(committed.date).await?;
456 log::debug!(
457 "finished Inline bucket fetch kind={} pages={page_count} end_seq={} updates={} events={}",
458 bucket_kind(key),
459 committed.seq,
460 update_count,
461 events.len()
462 );
463 Ok(events)
464 }
465
466 async fn repair_cold_bucket<H: SyncHost>(
467 &self,
468 host: &H,
469 key: SyncBucketKey,
470 target_seq: i64,
471 target_date: i64,
472 ) -> BackendResult<Vec<ClientEvent>> {
473 let mut events = host.repair_bucket(key).await?;
474 events.extend(
475 self.commit_cold_pointer(host, key, target_seq, target_date)
476 .await?,
477 );
478 Ok(events)
479 }
480
481 async fn commit_cold_pointer<H: SyncHost>(
482 &self,
483 host: &H,
484 key: SyncBucketKey,
485 target_seq: i64,
486 target_date: i64,
487 ) -> BackendResult<Vec<ClientEvent>> {
488 let committed = SyncBucketState {
489 seq: target_seq,
490 date: target_date,
491 };
492 let events = self
493 .commit_sync_batch(host, key, committed, Vec::new(), None)
494 .await?;
495 self.update_last_sync_date(committed.date).await?;
496 Ok(events)
497 }
498
499 async fn commit_sync_batch<H: SyncHost>(
500 &self,
501 host: &H,
502 key: SyncBucketKey,
503 committed_state: SyncBucketState,
504 updates: Vec<proto::Update>,
505 sidecars: Option<proto::UpdateSidecars>,
506 ) -> BackendResult<Vec<ClientEvent>> {
507 let payload = encode_pending_payload(&updates, sidecars.as_ref())?;
508 self.store
509 .save_pending_sync_batch(PendingSyncBatch {
510 key,
511 committed_state,
512 payload,
513 })
514 .await
515 .map_err(store_error_to_backend)?;
516 let events = host.apply_sync_batch(updates, sidecars).await?;
517 self.store
518 .commit_pending_sync_batch(key, committed_state)
519 .await
520 .map_err(store_error_to_backend)?;
521 Ok(events)
522 }
523
524 async fn recover_pending_batches<H: SyncHost>(
525 &self,
526 host: &H,
527 ) -> BackendResult<Vec<ClientEvent>> {
528 let batches = self
529 .store
530 .pending_sync_batches()
531 .await
532 .map_err(store_error_to_backend)?;
533 let mut events = Vec::new();
534 for batch in batches {
535 let (updates, sidecars) = decode_pending_payload(&batch.payload)?;
536 events.extend(host.apply_sync_batch(updates, sidecars).await?);
537 self.store
538 .commit_pending_sync_batch(batch.key, batch.committed_state)
539 .await
540 .map_err(store_error_to_backend)?;
541 self.update_last_sync_date(batch.committed_state.date)
542 .await?;
543 }
544 Ok(events)
545 }
546
547 async fn bucket_lock(&self, key: SyncBucketKey) -> Arc<Mutex<()>> {
548 let mut locks = self.bucket_locks.lock().await;
549 locks
550 .entry(key)
551 .or_insert_with(|| Arc::new(Mutex::new(())))
552 .clone()
553 }
554
555 async fn prepared_sync_state(&self) -> BackendResult<SyncState> {
556 let mut state = self
557 .store
558 .sync_state()
559 .await
560 .map_err(store_error_to_backend)?;
561 let now = now_seconds();
562 if state.last_sync_date == 0
563 || now.saturating_sub(state.last_sync_date) > self.config.stale_state_max_age_seconds
564 {
565 state.last_sync_date = now.saturating_sub(self.config.initial_lookback_seconds);
566 self.store
567 .save_sync_state(state)
568 .await
569 .map_err(store_error_to_backend)?;
570 }
571 Ok(state)
572 }
573
574 async fn update_last_sync_date(&self, max_applied_date: i64) -> BackendResult<()> {
575 if max_applied_date <= 0 {
576 return Ok(());
577 }
578 let _guard = self.sync_state_lock.lock().await;
579 let current = self
580 .store
581 .sync_state()
582 .await
583 .map_err(store_error_to_backend)?;
584 let proposed = max_applied_date.saturating_sub(self.config.last_sync_safety_gap_seconds);
585 if proposed <= current.last_sync_date {
586 return Ok(());
587 }
588 self.store
589 .save_sync_state(SyncState {
590 last_sync_date: proposed,
591 })
592 .await
593 .map_err(store_error_to_backend)
594 }
595}
596
597fn bucket_key_for_update(update: &proto::Update) -> Option<SyncBucketKey> {
598 use proto::update::Update;
599 match update.update.as_ref()? {
600 Update::NewMessage(value) => value
601 .message
602 .as_ref()?
603 .peer_id
604 .as_ref()
605 .and_then(chat_bucket),
606 Update::EditMessage(value) => value
607 .message
608 .as_ref()?
609 .peer_id
610 .as_ref()
611 .and_then(chat_bucket),
612 Update::DeleteMessages(value) => value.peer_id.as_ref().and_then(chat_bucket),
613 Update::ClearChatHistory(value) => match value.target.as_ref()? {
614 proto::update_clear_chat_history::Target::PeerId(peer) => chat_bucket(peer),
615 proto::update_clear_chat_history::Target::SpaceId(space_id) => {
616 Some(SyncBucketKey::Space {
617 space_id: InlineId::new(*space_id),
618 })
619 }
620 },
621 Update::MessageAttachment(value) => value.peer_id.as_ref().and_then(chat_bucket),
622 Update::UpdateReaction(value) => value
623 .reaction
624 .as_ref()
625 .map(|reaction| chat_key(reaction.chat_id)),
626 Update::DeleteReaction(value) => Some(chat_key(value.chat_id)),
627 Update::DeleteChat(value) => value.peer_id.as_ref().and_then(chat_bucket),
628 Update::MarkAsUnread(value) => value.peer_id.as_ref().and_then(chat_bucket),
629 Update::SpaceMemberAdd(value) => value
630 .member
631 .as_ref()
632 .map(|member| space_key(member.space_id)),
633 Update::SpaceMemberDelete(value) => Some(space_key(value.space_id)),
634 Update::SpaceMemberUpdate(value) => value
635 .member
636 .as_ref()
637 .map(|member| space_key(member.space_id)),
638 Update::JoinSpace(_)
639 | Update::UpdateUserStatus(_)
640 | Update::UpdateUserSettings(_)
641 | Update::UpdatedUser(_)
642 | Update::DialogArchived(_)
643 | Update::DialogNotificationSettings(_)
644 | Update::DialogFollowMode(_)
645 | Update::UpdateReadMaxId(_)
646 | Update::ChatOpen(_) => Some(SyncBucketKey::User),
647 Update::NewChat(value) => value.chat.as_ref()?.peer_id.as_ref().and_then(chat_bucket),
648 Update::ChatMoved(value) => value.chat.as_ref()?.peer_id.as_ref().and_then(chat_bucket),
649 Update::ParticipantAdd(value) => Some(chat_key(value.chat_id)),
650 Update::ParticipantDelete(value) => Some(chat_key(value.chat_id)),
651 Update::ChatVisibility(value) => Some(chat_key(value.chat_id)),
652 Update::ChatInfo(value) => Some(chat_key(value.chat_id)),
653 Update::PinnedMessages(value) => value.peer_id.as_ref().and_then(chat_bucket),
654 Update::ChatSkipPts(value) => Some(chat_key(value.chat_id)),
655 _ => None,
656 }
657}
658
659fn chat_hint_bucket_key(hint: &proto::UpdateChatHasNewUpdates) -> Option<SyncBucketKey> {
660 hint.peer_id
661 .as_ref()
662 .and_then(chat_bucket)
663 .or_else(|| (hint.chat_id != 0).then(|| chat_key(hint.chat_id)))
664}
665
666fn chat_bucket(peer: &proto::Peer) -> Option<SyncBucketKey> {
667 let peer = match peer.r#type.as_ref()? {
668 proto::peer::Type::User(user) => SyncBucketPeer::User {
669 user_id: InlineId::new(user.user_id),
670 },
671 proto::peer::Type::Chat(chat) => SyncBucketPeer::Chat {
672 chat_id: InlineId::new(chat.chat_id),
673 },
674 };
675 Some(SyncBucketKey::Chat { peer })
676}
677
678fn chat_key(chat_id: i64) -> SyncBucketKey {
679 SyncBucketKey::Chat {
680 peer: SyncBucketPeer::Chat {
681 chat_id: InlineId::new(chat_id),
682 },
683 }
684}
685
686fn space_key(space_id: i64) -> SyncBucketKey {
687 SyncBucketKey::Space {
688 space_id: InlineId::new(space_id),
689 }
690}
691
692fn protocol_bucket(key: SyncBucketKey) -> proto::UpdateBucket {
693 let r#type = match key {
694 SyncBucketKey::User => proto::update_bucket::Type::User(proto::UpdateBucketUser {}),
695 SyncBucketKey::Space { space_id } => {
696 proto::update_bucket::Type::Space(proto::UpdateBucketSpace {
697 space_id: space_id.get(),
698 })
699 }
700 SyncBucketKey::Chat { peer } => {
701 let r#type = match peer {
702 SyncBucketPeer::User { user_id } => {
703 proto::input_peer::Type::User(proto::InputPeerUser {
704 user_id: user_id.get(),
705 })
706 }
707 SyncBucketPeer::Chat { chat_id } => {
708 proto::input_peer::Type::Chat(proto::InputPeerChat {
709 chat_id: chat_id.get(),
710 })
711 }
712 };
713 proto::update_bucket::Type::Chat(proto::UpdateBucketChat {
714 peer_id: Some(proto::InputPeer {
715 r#type: Some(r#type),
716 }),
717 })
718 }
719 };
720 proto::UpdateBucket {
721 r#type: Some(r#type),
722 }
723}
724
725fn update_seq(update: &proto::Update) -> i64 {
726 i64::from(update.seq.unwrap_or_default())
727}
728
729fn max_update_date(updates: &[proto::Update]) -> i64 {
730 updates
731 .iter()
732 .filter_map(|update| update.date)
733 .max()
734 .unwrap_or_default()
735}
736
737fn merge_sidecars(target: &mut proto::UpdateSidecars, page: proto::UpdateSidecars) {
738 target.users.extend(page.users);
739 target.chats.extend(page.chats);
740 target.dialogs.extend(page.dialogs);
741 target.spaces.extend(page.spaces);
742}
743
744fn bucket_sort_key(key: SyncBucketKey) -> (u8, i64) {
745 match key {
746 SyncBucketKey::User => (0, 0),
747 SyncBucketKey::Space { space_id } => (1, space_id.get()),
748 SyncBucketKey::Chat {
749 peer: SyncBucketPeer::User { user_id },
750 } => (2, user_id.get()),
751 SyncBucketKey::Chat {
752 peer: SyncBucketPeer::Chat { chat_id },
753 } => (3, chat_id.get()),
754 }
755}
756
757fn bucket_kind(key: SyncBucketKey) -> &'static str {
758 match key {
759 SyncBucketKey::User => "user",
760 SyncBucketKey::Space { .. } => "space",
761 SyncBucketKey::Chat { .. } => "chat",
762 }
763}
764
765fn store_error_to_backend(error: StoreError) -> BackendError {
766 BackendError::new(error.category, error.message)
767}
768
769fn now_seconds() -> i64 {
770 SystemTime::now()
771 .duration_since(UNIX_EPOCH)
772 .unwrap_or_default()
773 .as_secs() as i64
774}
775
776const PENDING_SYNC_PAYLOAD_MAGIC: &[u8; 4] = b"ISB1";
777const MAX_PENDING_SYNC_ITEMS: usize = 100_000;
778const MAX_PENDING_SYNC_ITEM_BYTES: usize = 64 * 1024 * 1024;
779
780fn encode_pending_payload(
781 updates: &[proto::Update],
782 sidecars: Option<&proto::UpdateSidecars>,
783) -> BackendResult<Vec<u8>> {
784 if updates.len() > MAX_PENDING_SYNC_ITEMS {
785 return Err(BackendError::new(
786 ClientErrorCategory::Internal,
787 "sync batch is too large to journal",
788 ));
789 }
790 let mut payload = Vec::new();
791 payload.extend_from_slice(PENDING_SYNC_PAYLOAD_MAGIC);
792 append_u32(&mut payload, updates.len())?;
793 for update in updates {
794 let encoded = update.encode_to_vec();
795 append_bytes(&mut payload, &encoded)?;
796 }
797 match sidecars {
798 Some(sidecars) => {
799 payload.push(1);
800 append_bytes(&mut payload, &sidecars.encode_to_vec())?;
801 }
802 None => payload.push(0),
803 }
804 Ok(payload)
805}
806
807fn decode_pending_payload(
808 payload: &[u8],
809) -> BackendResult<(Vec<proto::Update>, Option<proto::UpdateSidecars>)> {
810 if !payload.starts_with(PENDING_SYNC_PAYLOAD_MAGIC) {
811 return Err(invalid_pending_payload("unsupported journal version"));
812 }
813 let mut offset = PENDING_SYNC_PAYLOAD_MAGIC.len();
814 let count = read_u32(payload, &mut offset)?;
815 if count > MAX_PENDING_SYNC_ITEMS {
816 return Err(invalid_pending_payload("update count exceeds safety limit"));
817 }
818 let mut updates = Vec::with_capacity(count);
819 for _ in 0..count {
820 let bytes = read_bytes(payload, &mut offset)?;
821 updates.push(
822 proto::Update::decode(bytes)
823 .map_err(|_| invalid_pending_payload("update protobuf is invalid"))?,
824 );
825 }
826 let sidecars = match payload.get(offset).copied() {
827 Some(0) => {
828 offset += 1;
829 None
830 }
831 Some(1) => {
832 offset += 1;
833 let bytes = read_bytes(payload, &mut offset)?;
834 Some(
835 proto::UpdateSidecars::decode(bytes)
836 .map_err(|_| invalid_pending_payload("sidecars protobuf is invalid"))?,
837 )
838 }
839 _ => return Err(invalid_pending_payload("sidecars flag is invalid")),
840 };
841 if offset != payload.len() {
842 return Err(invalid_pending_payload("journal has trailing bytes"));
843 }
844 Ok((updates, sidecars))
845}
846
847fn append_u32(payload: &mut Vec<u8>, value: usize) -> BackendResult<()> {
848 let value = u32::try_from(value).map_err(|_| {
849 BackendError::new(
850 ClientErrorCategory::Internal,
851 "sync journal length exceeds u32",
852 )
853 })?;
854 payload.extend_from_slice(&value.to_le_bytes());
855 Ok(())
856}
857
858fn append_bytes(payload: &mut Vec<u8>, bytes: &[u8]) -> BackendResult<()> {
859 if bytes.len() > MAX_PENDING_SYNC_ITEM_BYTES {
860 return Err(BackendError::new(
861 ClientErrorCategory::Internal,
862 "sync journal item exceeds safety limit",
863 ));
864 }
865 append_u32(payload, bytes.len())?;
866 payload.extend_from_slice(bytes);
867 Ok(())
868}
869
870fn read_u32(payload: &[u8], offset: &mut usize) -> BackendResult<usize> {
871 let end = offset.saturating_add(4);
872 let bytes = payload
873 .get(*offset..end)
874 .ok_or_else(|| invalid_pending_payload("journal is truncated"))?;
875 *offset = end;
876 Ok(u32::from_le_bytes(
877 bytes
878 .try_into()
879 .map_err(|_| invalid_pending_payload("length is invalid"))?,
880 ) as usize)
881}
882
883fn read_bytes<'a>(payload: &'a [u8], offset: &mut usize) -> BackendResult<&'a [u8]> {
884 let length = read_u32(payload, offset)?;
885 if length > MAX_PENDING_SYNC_ITEM_BYTES {
886 return Err(invalid_pending_payload("journal item exceeds safety limit"));
887 }
888 let end = offset.saturating_add(length);
889 let bytes = payload
890 .get(*offset..end)
891 .ok_or_else(|| invalid_pending_payload("journal item is truncated"))?;
892 *offset = end;
893 Ok(bytes)
894}
895
896fn invalid_pending_payload(message: &'static str) -> BackendError {
897 BackendError::new(
898 ClientErrorCategory::ProtocolMismatch,
899 format!("invalid pending sync journal: {message}"),
900 )
901}
902
903#[cfg(test)]
904mod tests {
905 use std::{
906 collections::VecDeque,
907 sync::{
908 Arc,
909 atomic::{AtomicBool, AtomicUsize, Ordering},
910 },
911 };
912
913 use super::*;
914 use crate::InMemoryStore;
915
916 #[derive(Clone, Debug)]
917 struct FakeHost {
918 state: proto::GetUpdatesStateResult,
919 responses: Arc<Mutex<VecDeque<BackendResult<proto::GetUpdatesResult>>>>,
920 requests: Arc<Mutex<Vec<proto::GetUpdatesInput>>>,
921 applied: Arc<Mutex<Vec<Vec<proto::Update>>>>,
922 repaired_buckets: Arc<Mutex<Vec<SyncBucketKey>>>,
923 fail_apply: Arc<AtomicBool>,
924 fetch_delay_ms: u64,
925 fetches_in_flight: Arc<AtomicUsize>,
926 max_fetches_in_flight: Arc<AtomicUsize>,
927 }
928
929 impl FakeHost {
930 fn new(responses: Vec<proto::GetUpdatesResult>) -> Self {
931 Self {
932 state: proto::GetUpdatesStateResult {
933 date: 100,
934 updates_found: Some(true),
935 },
936 responses: Arc::new(Mutex::new(
937 responses.into_iter().map(Ok).collect::<VecDeque<_>>(),
938 )),
939 requests: Arc::new(Mutex::new(Vec::new())),
940 applied: Arc::new(Mutex::new(Vec::new())),
941 repaired_buckets: Arc::new(Mutex::new(Vec::new())),
942 fail_apply: Arc::new(AtomicBool::new(false)),
943 fetch_delay_ms: 0,
944 fetches_in_flight: Arc::new(AtomicUsize::new(0)),
945 max_fetches_in_flight: Arc::new(AtomicUsize::new(0)),
946 }
947 }
948
949 fn failing_apply(mut self) -> Self {
950 self.fail_apply = Arc::new(AtomicBool::new(true));
951 self
952 }
953
954 fn with_fetch_delay(mut self, delay_ms: u64) -> Self {
955 self.fetch_delay_ms = delay_ms;
956 self
957 }
958 }
959
960 impl SyncHost for FakeHost {
961 fn get_updates_state(
962 &self,
963 _date: i64,
964 ) -> BoxFuture<'static, BackendResult<proto::GetUpdatesStateResult>> {
965 let state = self.state.clone();
966 Box::pin(async move { Ok(state) })
967 }
968
969 fn get_updates(
970 &self,
971 input: proto::GetUpdatesInput,
972 ) -> BoxFuture<'static, BackendResult<proto::GetUpdatesResult>> {
973 let host = self.clone();
974 Box::pin(async move {
975 let in_flight = host.fetches_in_flight.fetch_add(1, Ordering::SeqCst) + 1;
976 host.max_fetches_in_flight
977 .fetch_max(in_flight, Ordering::SeqCst);
978 if host.fetch_delay_ms > 0 {
979 tokio::time::sleep(std::time::Duration::from_millis(host.fetch_delay_ms)).await;
980 }
981 host.requests.lock().await.push(input);
982 let response = host.responses.lock().await.pop_front().unwrap_or_else(|| {
983 Err(BackendError::new(
984 ClientErrorCategory::Internal,
985 "missing fake getUpdates response",
986 ))
987 });
988 host.fetches_in_flight.fetch_sub(1, Ordering::SeqCst);
989 response
990 })
991 }
992
993 fn apply_sync_batch(
994 &self,
995 updates: Vec<proto::Update>,
996 _sidecars: Option<proto::UpdateSidecars>,
997 ) -> BoxFuture<'static, BackendResult<Vec<ClientEvent>>> {
998 let host = self.clone();
999 Box::pin(async move {
1000 if host.fail_apply.load(Ordering::Relaxed) {
1001 return Err(BackendError::new(
1002 ClientErrorCategory::Internal,
1003 "fake apply failure",
1004 ));
1005 }
1006 host.applied.lock().await.push(updates.clone());
1007 Ok(updates
1008 .iter()
1009 .filter_map(|update| match update.update.as_ref() {
1010 Some(proto::update::Update::NewMessage(update)) => {
1011 let message = update.message.as_ref()?;
1012 Some(ClientEvent::MessageUpserted {
1013 chat_id: InlineId::new(message.chat_id),
1014 message_id: InlineId::new(message.id),
1015 })
1016 }
1017 _ => None,
1018 })
1019 .collect())
1020 })
1021 }
1022
1023 fn repair_bucket(
1024 &self,
1025 key: SyncBucketKey,
1026 ) -> BoxFuture<'static, BackendResult<Vec<ClientEvent>>> {
1027 let host = self.clone();
1028 Box::pin(async move {
1029 host.repaired_buckets.lock().await.push(key);
1030 Ok(Vec::new())
1031 })
1032 }
1033 }
1034
1035 #[tokio::test]
1036 async fn hint_only_recovery_pages_to_target_and_persists_cursor() {
1037 let store = Arc::new(InMemoryStore::new());
1038 let key = chat_key(7);
1039 store
1040 .save_sync_bucket_state(key, SyncBucketState { seq: 1, date: 10 })
1041 .await
1042 .unwrap();
1043 let host = FakeHost::new(vec![
1044 updates_result(vec![message_update(2, 20, 7, 102)], 2, 20, false),
1045 updates_result(vec![message_update(3, 30, 7, 103)], 3, 30, true),
1046 ]);
1047 let sync = SyncManager::new(store.clone(), SyncConfig::default());
1048
1049 let events = sync
1050 .process_realtime(&host, vec![chat_hint(7, 3)])
1051 .await
1052 .unwrap();
1053
1054 assert_eq!(events.len(), 2);
1055 assert_eq!(
1056 store.sync_bucket_state(key).await.unwrap(),
1057 SyncBucketState { seq: 3, date: 30 }
1058 );
1059 let requests = host.requests.lock().await;
1060 assert_eq!(requests.len(), 2);
1061 assert_eq!(requests[0].start_seq, 1);
1062 assert_eq!(requests[0].seq_end, 3);
1063 assert_eq!(requests[1].start_seq, 2);
1064 assert_eq!(requests[1].seq_end, 3);
1065 }
1066
1067 #[tokio::test]
1068 async fn gap_recovery_merges_buffered_realtime_update_before_commit() {
1069 let store = Arc::new(InMemoryStore::new());
1070 let key = chat_key(7);
1071 store
1072 .save_sync_bucket_state(key, SyncBucketState { seq: 1, date: 10 })
1073 .await
1074 .unwrap();
1075 let host = FakeHost::new(vec![updates_result(
1076 vec![message_update(2, 20, 7, 102)],
1077 3,
1078 30,
1079 true,
1080 )]);
1081 let sync = SyncManager::new(store.clone(), SyncConfig::default());
1082
1083 sync.process_realtime(&host, vec![message_update(3, 30, 7, 103)])
1084 .await
1085 .unwrap();
1086
1087 let applied = host.applied.lock().await;
1088 assert_eq!(applied.len(), 1);
1089 assert_eq!(
1090 applied[0].iter().map(update_seq).collect::<Vec<_>>(),
1091 vec![2, 3]
1092 );
1093 assert_eq!(
1094 store.sync_bucket_state(key).await.unwrap(),
1095 SyncBucketState { seq: 3, date: 30 }
1096 );
1097 }
1098
1099 #[tokio::test]
1100 async fn apply_failure_does_not_advance_bucket_cursor() {
1101 let store = Arc::new(InMemoryStore::new());
1102 let key = chat_key(7);
1103 store
1104 .save_sync_bucket_state(key, SyncBucketState { seq: 1, date: 10 })
1105 .await
1106 .unwrap();
1107 let host = FakeHost::new(vec![updates_result(
1108 vec![message_update(2, 20, 7, 101)],
1109 2,
1110 20,
1111 true,
1112 )])
1113 .failing_apply();
1114 let sync = SyncManager::new(store.clone(), SyncConfig::default());
1115
1116 let error = sync
1117 .process_realtime(&host, vec![chat_hint(7, 2)])
1118 .await
1119 .unwrap_err();
1120
1121 assert_eq!(error.category, ClientErrorCategory::Internal);
1122 assert_eq!(
1123 store.sync_bucket_state(key).await.unwrap(),
1124 SyncBucketState { seq: 1, date: 10 }
1125 );
1126 assert_eq!(store.pending_sync_batches().await.unwrap().len(), 1);
1127
1128 host.fail_apply.store(false, Ordering::Relaxed);
1129 let recovered = sync.process_realtime(&host, Vec::new()).await.unwrap();
1130 assert_eq!(recovered.len(), 1);
1131 assert_eq!(
1132 store.sync_bucket_state(key).await.unwrap(),
1133 SyncBucketState { seq: 2, date: 20 }
1134 );
1135 assert!(store.pending_sync_batches().await.unwrap().is_empty());
1136 }
1137
1138 #[test]
1139 fn pending_sync_payload_round_trips_updates_and_sidecars() {
1140 let updates = vec![message_update(2, 20, 7, 102)];
1141 let sidecars = proto::UpdateSidecars {
1142 users: vec![proto::User {
1143 id: 42,
1144 ..Default::default()
1145 }],
1146 ..Default::default()
1147 };
1148 let payload = encode_pending_payload(&updates, Some(&sidecars)).unwrap();
1149
1150 let (decoded_updates, decoded_sidecars) = decode_pending_payload(&payload).unwrap();
1151 assert_eq!(decoded_updates, updates);
1152 assert_eq!(decoded_sidecars, Some(sidecars));
1153 }
1154
1155 #[tokio::test]
1156 async fn empty_result_trusts_hint_pointer() {
1157 let store = Arc::new(InMemoryStore::new());
1158 let key = chat_key(7);
1159 store
1160 .save_sync_bucket_state(key, SyncBucketState { seq: 5, date: 50 })
1161 .await
1162 .unwrap();
1163 let host = FakeHost::new(vec![proto::GetUpdatesResult {
1164 updates: Vec::new(),
1165 seq: 6,
1166 date: 80,
1167 r#final: None,
1168 result_type: proto::get_updates_result::ResultType::Empty as i32,
1169 sidecars: None,
1170 }]);
1171 let sync = SyncManager::new(store.clone(), SyncConfig::default());
1172
1173 sync.process_realtime(&host, vec![chat_hint(7, 8)])
1174 .await
1175 .unwrap();
1176
1177 assert_eq!(
1178 store.sync_bucket_state(key).await.unwrap(),
1179 SyncBucketState { seq: 8, date: 80 }
1180 );
1181 }
1182
1183 #[tokio::test]
1184 async fn cold_chat_too_long_repairs_snapshot_and_advances_to_hint() {
1185 let store = Arc::new(InMemoryStore::new());
1186 let key = chat_key(7);
1187 let host = FakeHost::new(vec![proto::GetUpdatesResult {
1188 updates: Vec::new(),
1189 seq: 400,
1190 date: 80,
1191 r#final: Some(false),
1192 result_type: proto::get_updates_result::ResultType::TooLong as i32,
1193 sidecars: None,
1194 }]);
1195 let sync = SyncManager::new(store.clone(), SyncConfig::default());
1196
1197 sync.process_realtime(&host, vec![chat_hint(7, 500)])
1198 .await
1199 .unwrap();
1200
1201 assert_eq!(host.repaired_buckets.lock().await.as_slice(), &[key]);
1202 assert_eq!(
1203 store.sync_bucket_state(key).await.unwrap(),
1204 SyncBucketState { seq: 500, date: 80 }
1205 );
1206 assert_eq!(host.requests.lock().await.len(), 1);
1207 }
1208
1209 #[tokio::test]
1210 async fn cold_user_and_space_too_long_require_snapshots_before_advancing() {
1211 for key in [
1212 SyncBucketKey::User,
1213 SyncBucketKey::Space {
1214 space_id: InlineId::new(9),
1215 },
1216 ] {
1217 let store = Arc::new(InMemoryStore::new());
1218 let host = FakeHost::new(vec![proto::GetUpdatesResult {
1219 updates: Vec::new(),
1220 seq: 400,
1221 date: 80,
1222 r#final: Some(false),
1223 result_type: proto::get_updates_result::ResultType::TooLong as i32,
1224 sidecars: None,
1225 }]);
1226 let sync = SyncManager::new(store.clone(), SyncConfig::default());
1227
1228 sync.fetch_bucket(
1229 &host,
1230 key,
1231 SyncBucketState::default(),
1232 BTreeMap::new(),
1233 Some(500),
1234 )
1235 .await
1236 .unwrap();
1237
1238 assert_eq!(host.repaired_buckets.lock().await.as_slice(), &[key]);
1239 assert_eq!(
1240 store.sync_bucket_state(key).await.unwrap(),
1241 SyncBucketState { seq: 500, date: 80 }
1242 );
1243 }
1244 }
1245
1246 #[tokio::test]
1247 async fn independent_bucket_fetches_use_configured_concurrency() {
1248 let store = Arc::new(InMemoryStore::new());
1249 let empty = |date| proto::GetUpdatesResult {
1250 updates: Vec::new(),
1251 seq: 1,
1252 date,
1253 r#final: Some(true),
1254 result_type: proto::get_updates_result::ResultType::Empty as i32,
1255 sidecars: None,
1256 };
1257 let host = FakeHost::new(vec![empty(100), empty(200)]).with_fetch_delay(25);
1258 let sync = SyncManager::new(
1259 store.clone(),
1260 SyncConfig {
1261 max_concurrent_bucket_fetches: 2,
1262 ..SyncConfig::default()
1263 },
1264 );
1265
1266 sync.process_realtime(&host, vec![chat_hint(7, 1), chat_hint(8, 1)])
1267 .await
1268 .unwrap();
1269
1270 assert_eq!(host.max_fetches_in_flight.load(Ordering::SeqCst), 2);
1271 assert_eq!(store.sync_bucket_state(chat_key(7)).await.unwrap().seq, 1);
1272 assert_eq!(store.sync_bucket_state(chat_key(8)).await.unwrap().seq, 1);
1273 assert_eq!(store.sync_state().await.unwrap().last_sync_date, 185);
1274 }
1275
1276 fn updates_result(
1277 updates: Vec<proto::Update>,
1278 seq: i64,
1279 date: i64,
1280 final_page: bool,
1281 ) -> proto::GetUpdatesResult {
1282 proto::GetUpdatesResult {
1283 updates,
1284 seq,
1285 date,
1286 r#final: Some(final_page),
1287 result_type: proto::get_updates_result::ResultType::Slice as i32,
1288 sidecars: None,
1289 }
1290 }
1291
1292 fn chat_hint(chat_id: i64, seq: i32) -> proto::Update {
1293 proto::Update {
1294 seq: None,
1295 date: None,
1296 update: Some(proto::update::Update::ChatHasNewUpdates(
1297 proto::UpdateChatHasNewUpdates {
1298 chat_id,
1299 update_seq: seq,
1300 peer_id: Some(chat_peer(chat_id)),
1301 },
1302 )),
1303 }
1304 }
1305
1306 fn message_update(seq: i32, date: i64, chat_id: i64, message_id: i64) -> proto::Update {
1307 proto::Update {
1308 seq: Some(seq),
1309 date: Some(date),
1310 update: Some(proto::update::Update::NewMessage(proto::UpdateNewMessage {
1311 message: Some(proto::Message {
1312 id: message_id,
1313 chat_id,
1314 peer_id: Some(chat_peer(chat_id)),
1315 ..Default::default()
1316 }),
1317 })),
1318 }
1319 }
1320
1321 fn chat_peer(chat_id: i64) -> proto::Peer {
1322 proto::Peer {
1323 r#type: Some(proto::peer::Type::Chat(proto::PeerChat { chat_id })),
1324 }
1325 }
1326}