1use crate::source::{SyncContext, SyncResult, SyncSource, SyncThrottle};
2use crate::state_store::SyncStateStore;
3use crate::{SyncPolicy, SyncTarget, health_of};
4use origin_domain::{
5 AccountId, AppError, Clock, ConnectorId, ErrorKind, Health, Result, SyncId, SyncOutcome,
6 SyncState, ThrottleReason,
7};
8use origin_events::{EventBus, PlatformEvent, SyncCompleted, SyncFailed};
9use origin_storage::Storage;
10use std::collections::BTreeMap;
11use std::sync::Arc;
12use std::sync::RwLock;
13use std::sync::atomic::{AtomicU64, Ordering};
14use time::{Duration, OffsetDateTime};
15use tokio::sync::Mutex;
16use tokio_util::sync::CancellationToken;
17
18const TICK: std::time::Duration = std::time::Duration::from_secs(5);
23
24#[derive(Debug)]
25struct Registration {
26 policy: SyncPolicy,
27 source: Arc<dyn SyncSource>,
28 running: Arc<Mutex<()>>,
30 cancel: CancellationToken,
31}
32
33#[derive(Debug, Clone)]
35pub struct SyncEngine {
36 targets: Arc<RwLock<BTreeMap<SyncTarget, Registration>>>,
37 state: SyncStateStore,
38 clock: Arc<dyn Clock>,
39 events: EventBus,
40 seed: Arc<AtomicU64>,
42}
43
44impl SyncEngine {
45 pub fn new(storage: Arc<dyn Storage>, clock: Arc<dyn Clock>, events: EventBus) -> Self {
46 let seed = clock.now().unix_timestamp_nanos() as u64 | 1;
47
48 Self {
49 targets: Arc::new(RwLock::new(BTreeMap::new())),
50 state: SyncStateStore::new(storage, clock.clone()),
51 clock,
52 events,
53 seed: Arc::new(AtomicU64::new(seed)),
54 }
55 }
56
57 pub fn register(&self, target: SyncTarget, policy: SyncPolicy, source: Arc<dyn SyncSource>) {
62 tracing::debug!(%target, interval = ?policy.interval, "sync target registered");
63
64 self.write().insert(
65 target,
66 Registration {
67 policy,
68 source,
69 running: Arc::new(Mutex::new(())),
70 cancel: CancellationToken::new(),
71 },
72 );
73 }
74
75 pub fn unregister(&self, target: &SyncTarget) {
77 if let Some(registration) = self.write().remove(target) {
78 registration.cancel.cancel();
79 }
80 }
81
82 pub fn targets(&self) -> Vec<SyncTarget> {
83 self.read().keys().cloned().collect()
84 }
85
86 pub fn policy(&self, target: &SyncTarget) -> Option<SyncPolicy> {
88 self.read()
89 .get(target)
90 .map(|registration| registration.policy)
91 }
92
93 fn read(&self) -> std::sync::RwLockReadGuard<'_, BTreeMap<SyncTarget, Registration>> {
94 self.targets
95 .read()
96 .unwrap_or_else(|poisoned| poisoned.into_inner())
97 }
98
99 fn write(&self) -> std::sync::RwLockWriteGuard<'_, BTreeMap<SyncTarget, Registration>> {
100 self.targets
101 .write()
102 .unwrap_or_else(|poisoned| poisoned.into_inner())
103 }
104
105 pub async fn state(&self, target: &SyncTarget) -> Result<SyncState> {
106 self.state.load(target).await
107 }
108
109 pub async fn due_at(&self, target: &SyncTarget) -> Result<OffsetDateTime> {
114 let policy = self
115 .read()
116 .get(target)
117 .map(|registration| registration.policy)
118 .ok_or_else(|| AppError::validation(format!("unknown sync target {target}")))?;
119
120 let state = self.state.load(target).await?;
121 Ok(self.due_at_for(&state, &policy))
122 }
123
124 fn due_at_for(&self, state: &SyncState, policy: &SyncPolicy) -> OffsetDateTime {
125 let Some(last_attempt) = state.last_attempt else {
126 return OffsetDateTime::UNIX_EPOCH;
128 };
129
130 let delay = match (state.failure_streak, &state.last_outcome) {
131 (0, _) => policy.interval,
132 (
133 _,
134 Some(SyncOutcome::Failed {
135 kind: ErrorKind::Offline,
136 ..
137 }),
138 ) => policy.offline_retry,
139 (failures, _) => policy.backoff.delay_for(failures, self.next_random()),
140 };
141
142 let policy_due = last_attempt + delay;
146
147 match state.not_before {
150 Some(not_before) => policy_due.max(not_before),
151 None => policy_due,
152 }
153 }
154
155 pub async fn run_due(&self, now: OffsetDateTime) -> Vec<(SyncTarget, Result<SyncOutcome>)> {
160 let candidates: Vec<(SyncTarget, SyncPolicy)> = self
161 .read()
162 .iter()
163 .map(|(target, registration)| (target.clone(), registration.policy))
164 .collect();
165
166 let mut results = Vec::new();
167 let mut runs = Vec::new();
168 for (target, policy) in candidates {
169 let state = match self.state.load(&target).await {
170 Ok(state) => state,
171 Err(error) => {
172 results.push((target, Err(error)));
173 continue;
174 }
175 };
176
177 if now < self.due_at_for(&state, &policy) {
178 continue;
179 }
180
181 let engine = self.clone();
182 let run_target = target.clone();
183 runs.push((
184 target,
185 tokio::spawn(async move { engine.sync_if_still_due(&run_target, now).await }),
186 ));
187 }
188
189 for (target, run) in runs {
190 let outcome = match run.await {
191 Ok(Some(outcome)) => outcome,
192 Ok(None) => continue,
196 Err(error) => Err(AppError::internal(format!(
197 "sync task for {target} failed: {error}"
198 ))),
199 };
200 results.push((target, outcome));
201 }
202
203 results
204 }
205
206 async fn sync_if_still_due(
216 &self,
217 target: &SyncTarget,
218 now: OffsetDateTime,
219 ) -> Option<Result<SyncOutcome>> {
220 let (policy, source, running, cancel) = {
221 let targets = self.read();
222 let registration = targets.get(target)?;
223 (
224 registration.policy,
225 registration.source.clone(),
226 registration.running.clone(),
227 registration.cancel.clone(),
228 )
229 };
230
231 let _guard = running.lock().await;
232 let state = match self.state.load(target).await {
233 Ok(state) => state,
234 Err(error) => return Some(Err(error)),
235 };
236
237 if now < self.due_at_for(&state, &policy) {
238 tracing::debug!(%target, "sync skipped: no longer due once the lock was free");
239 return None;
240 }
241
242 Some(self.sync_with(target, policy, source, cancel, state).await)
243 }
244
245 pub async fn sync_if_due(&self, target: &SyncTarget) -> Result<Option<SyncOutcome>> {
254 let (policy, source, running, cancel) = {
255 let targets = self.read();
256 let registration = targets
257 .get(target)
258 .ok_or_else(|| AppError::validation(format!("unknown sync target {target}")))?;
259 (
260 registration.policy,
261 registration.source.clone(),
262 registration.running.clone(),
263 registration.cancel.clone(),
264 )
265 };
266
267 let _guard = running.lock().await;
268 let state = self.state.load(target).await?;
269 if let Some(last_attempt) = state.last_attempt
270 && self.clock.now() < last_attempt + policy.min_interval
271 {
272 tracing::debug!(%target, "sync skipped: ran too recently");
273 return Ok(None);
274 }
275
276 self.sync_with(target, policy, source, cancel, state)
277 .await
278 .map(Some)
279 }
280
281 pub async fn sync_now(&self, target: &SyncTarget) -> Result<SyncOutcome> {
287 let (policy, source, running, cancel) = {
288 let targets = self.read();
289 let registration = targets
290 .get(target)
291 .ok_or_else(|| AppError::validation(format!("unknown sync target {target}")))?;
292 (
293 registration.policy,
294 registration.source.clone(),
295 registration.running.clone(),
296 registration.cancel.clone(),
297 )
298 };
299
300 let _guard = running.lock().await;
301 let state = self.state.load(target).await?;
302 self.sync_with(target, policy, source, cancel, state).await
303 }
304
305 async fn sync_with(
306 &self,
307 target: &SyncTarget,
308 policy: SyncPolicy,
309 source: Arc<dyn SyncSource>,
310 cancel: CancellationToken,
311 state: SyncState,
312 ) -> Result<SyncOutcome> {
313 let sync_id = SyncId::generate();
314 let context = SyncContext::new(sync_id.clone(), target.clone(), state.clone(), cancel);
315
316 let span = tracing::info_span!(
317 "sync",
318 sync_id = sync_id.as_str(),
319 connector = target.connector.as_str(),
320 account_id = target.account.as_str(),
321 target = target.name.as_str(),
322 );
323 let _entered = span.enter();
324
325 let result = source.sync(&context).await;
326 let now = self.clock.now();
327 let mut state = state;
328
329 match result {
330 Ok(SyncResult::Updated(report)) => {
331 state.record(now, SyncOutcome::Updated);
332 if report.etag.is_some() {
335 state.etag = report.etag.clone();
336 }
337 if report.last_modified.is_some() {
338 state.last_modified = report.last_modified.clone();
339 }
340 self.apply_throttle(&mut state, report.throttle, &policy, target, now);
341 self.state.save(target, &state).await?;
342
343 tracing::debug!(changed = report.changed, "sync updated");
344 self.publish_completed(target, &sync_id, report.changed, now);
345 Ok(SyncOutcome::Updated)
346 }
347
348 Ok(SyncResult::NotModified) => {
349 state.record(now, SyncOutcome::NotModified);
350 state.clear_throttle();
354 self.state.save(target, &state).await?;
355
356 tracing::debug!("sync reported no change");
357 self.publish_completed(target, &sync_id, 0, now);
358 Ok(SyncOutcome::NotModified)
359 }
360
361 Err(error) => {
362 let outcome = SyncOutcome::Failed {
363 kind: error.kind(),
364 message: error.to_string(),
365 };
366 state.record(now, outcome.clone());
367
368 if let AppError::RateLimited {
372 retry_after_seconds: Some(seconds),
373 ..
374 } = &error
375 {
376 let delay = Duration::seconds(*seconds as i64).min(policy.max_throttle);
377 state.throttle_until(now + delay, ThrottleReason::RateLimited);
378 }
379
380 self.state.save(target, &state).await?;
381
382 let retry_at = Some(self.due_at_for(&state, &policy));
383 tracing::warn!(kind = ?error.kind(), %error, ?retry_at, "sync failed");
384
385 let _ = self.events.publish(PlatformEvent::SyncFailed(SyncFailed {
386 sync: sync_id,
387 connector: target.connector.clone(),
388 account: target.account.clone(),
389 kind: error.kind(),
390 message: error.to_string(),
391 retry_at,
392 }));
393
394 Err(error)
395 }
396 }
397 }
398
399 fn apply_throttle(
404 &self,
405 state: &mut SyncState,
406 throttle: Option<SyncThrottle>,
407 policy: &SyncPolicy,
408 target: &SyncTarget,
409 now: OffsetDateTime,
410 ) {
411 let Some(throttle) = throttle else {
412 state.clear_throttle();
413 return;
414 };
415
416 let delay = if throttle.delay > policy.max_throttle {
417 tracing::warn!(
418 %target,
419 requested = ?throttle.delay,
420 max = ?policy.max_throttle,
421 reason = ?throttle.reason,
422 "server-imposed throttle clamped to the policy maximum"
423 );
424 policy.max_throttle
425 } else {
426 throttle.delay
427 };
428
429 tracing::debug!(%target, ?delay, reason = ?throttle.reason, "sync throttled by the service");
430 state.throttle_until(now + delay, throttle.reason);
431 }
432
433 pub async fn health(&self) -> Health {
435 let now = self.clock.now();
436 let targets: Vec<(SyncTarget, SyncPolicy)> = self
437 .read()
438 .iter()
439 .map(|(target, registration)| (target.clone(), registration.policy))
440 .collect();
441
442 let mut states = Vec::new();
443 for (target, policy) in targets {
444 let state = self.state.load(&target).await.unwrap_or_default();
445 states.push(health_of(&state, &policy, now));
446 }
447
448 Health::aggregate(states)
449 }
450
451 pub async fn health_of_account(&self, connector: &ConnectorId, account: &AccountId) -> Health {
453 let now = self.clock.now();
454 let targets: Vec<(SyncTarget, SyncPolicy)> = self
455 .read()
456 .iter()
457 .filter(|(target, _)| &target.connector == connector && &target.account == account)
458 .map(|(target, registration)| (target.clone(), registration.policy))
459 .collect();
460
461 let mut states = Vec::new();
462 for (target, policy) in targets {
463 let state = self.state.load(&target).await.unwrap_or_default();
464 states.push(health_of(&state, &policy, now));
465 }
466
467 Health::aggregate(states)
468 }
469
470 pub async fn run(&self, stop: CancellationToken) {
480 tracing::debug!("sync scheduler started");
481 let mut ticker = tokio::time::interval(TICK);
482
483 loop {
484 tokio::select! {
485 _ = stop.cancelled() => break,
486 _ = ticker.tick() => {
487 self.run_due(self.clock.now()).await;
488 }
489 }
490 }
491
492 tracing::debug!("sync scheduler stopped");
493 }
494
495 fn publish_completed(
496 &self,
497 target: &SyncTarget,
498 sync_id: &SyncId,
499 changed: u64,
500 at: OffsetDateTime,
501 ) {
502 let _ = self
503 .events
504 .publish(PlatformEvent::SyncCompleted(SyncCompleted {
505 sync: sync_id.clone(),
506 connector: target.connector.clone(),
507 account: target.account.clone(),
508 changed,
509 at,
510 }));
511 }
512
513 fn next_random(&self) -> f64 {
515 let previous = self
516 .seed
517 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |seed| {
518 Some(
519 seed.wrapping_mul(6_364_136_223_846_793_005)
520 .wrapping_add(1_442_695_040_888_963_407),
521 )
522 });
523
524 let value = previous
525 .unwrap_or(0)
526 .wrapping_mul(6_364_136_223_846_793_005);
527 f64::from((value >> 40) as u32) / f64::from(1u32 << 24)
528 }
529}
530
531impl SyncEngine {
533 pub fn register_every(
534 &self,
535 target: SyncTarget,
536 interval: Duration,
537 source: Arc<dyn SyncSource>,
538 ) {
539 self.register(target, SyncPolicy::every(interval), source);
540 }
541}