1use crate::source::{SyncContext, SyncResult, SyncSource};
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,
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 last_attempt + delay
146 }
147
148 pub async fn run_due(&self, now: OffsetDateTime) -> Vec<(SyncTarget, Result<SyncOutcome>)> {
153 let candidates: Vec<(SyncTarget, SyncPolicy)> = self
154 .read()
155 .iter()
156 .map(|(target, registration)| (target.clone(), registration.policy))
157 .collect();
158
159 let mut results = Vec::new();
160 let mut runs = Vec::new();
161 for (target, policy) in candidates {
162 let state = match self.state.load(&target).await {
163 Ok(state) => state,
164 Err(error) => {
165 results.push((target, Err(error)));
166 continue;
167 }
168 };
169
170 if now < self.due_at_for(&state, &policy) {
171 continue;
172 }
173
174 let engine = self.clone();
175 let run_target = target.clone();
176 runs.push((
177 target,
178 tokio::spawn(async move { engine.sync_if_still_due(&run_target, now).await }),
179 ));
180 }
181
182 for (target, run) in runs {
183 let outcome = match run.await {
184 Ok(Some(outcome)) => outcome,
185 Ok(None) => continue,
189 Err(error) => Err(AppError::internal(format!(
190 "sync task for {target} failed: {error}"
191 ))),
192 };
193 results.push((target, outcome));
194 }
195
196 results
197 }
198
199 async fn sync_if_still_due(
209 &self,
210 target: &SyncTarget,
211 now: OffsetDateTime,
212 ) -> Option<Result<SyncOutcome>> {
213 let (policy, source, running, cancel) = {
214 let targets = self.read();
215 let registration = targets.get(target)?;
216 (
217 registration.policy,
218 registration.source.clone(),
219 registration.running.clone(),
220 registration.cancel.clone(),
221 )
222 };
223
224 let _guard = running.lock().await;
225 let state = match self.state.load(target).await {
226 Ok(state) => state,
227 Err(error) => return Some(Err(error)),
228 };
229
230 if now < self.due_at_for(&state, &policy) {
231 tracing::debug!(%target, "sync skipped: no longer due once the lock was free");
232 return None;
233 }
234
235 Some(self.sync_with(target, policy, source, cancel, state).await)
236 }
237
238 pub async fn sync_if_due(&self, target: &SyncTarget) -> Result<Option<SyncOutcome>> {
247 let (policy, source, running, cancel) = {
248 let targets = self.read();
249 let registration = targets
250 .get(target)
251 .ok_or_else(|| AppError::validation(format!("unknown sync target {target}")))?;
252 (
253 registration.policy,
254 registration.source.clone(),
255 registration.running.clone(),
256 registration.cancel.clone(),
257 )
258 };
259
260 let _guard = running.lock().await;
261 let state = self.state.load(target).await?;
262 if let Some(last_attempt) = state.last_attempt
263 && self.clock.now() < last_attempt + policy.min_interval
264 {
265 tracing::debug!(%target, "sync skipped: ran too recently");
266 return Ok(None);
267 }
268
269 self.sync_with(target, policy, source, cancel, state)
270 .await
271 .map(Some)
272 }
273
274 pub async fn sync_now(&self, target: &SyncTarget) -> Result<SyncOutcome> {
280 let (policy, source, running, cancel) = {
281 let targets = self.read();
282 let registration = targets
283 .get(target)
284 .ok_or_else(|| AppError::validation(format!("unknown sync target {target}")))?;
285 (
286 registration.policy,
287 registration.source.clone(),
288 registration.running.clone(),
289 registration.cancel.clone(),
290 )
291 };
292
293 let _guard = running.lock().await;
294 let state = self.state.load(target).await?;
295 self.sync_with(target, policy, source, cancel, state).await
296 }
297
298 async fn sync_with(
299 &self,
300 target: &SyncTarget,
301 policy: SyncPolicy,
302 source: Arc<dyn SyncSource>,
303 cancel: CancellationToken,
304 state: SyncState,
305 ) -> Result<SyncOutcome> {
306 let sync_id = SyncId::generate();
307 let context = SyncContext::new(sync_id.clone(), target.clone(), state.clone(), cancel);
308
309 let span = tracing::info_span!(
310 "sync",
311 sync_id = sync_id.as_str(),
312 connector = target.connector.as_str(),
313 account_id = target.account.as_str(),
314 target = target.name.as_str(),
315 );
316 let _entered = span.enter();
317
318 let result = source.sync(&context).await;
319 let now = self.clock.now();
320 let mut state = state;
321
322 match result {
323 Ok(SyncResult::Updated(report)) => {
324 state.record(now, SyncOutcome::Updated);
325 if report.etag.is_some() {
328 state.etag = report.etag.clone();
329 }
330 if report.last_modified.is_some() {
331 state.last_modified = report.last_modified.clone();
332 }
333 self.state.save(target, &state).await?;
334
335 tracing::debug!(changed = report.changed, "sync updated");
336 self.publish_completed(target, &sync_id, report.changed, now);
337 Ok(SyncOutcome::Updated)
338 }
339
340 Ok(SyncResult::NotModified) => {
341 state.record(now, SyncOutcome::NotModified);
342 self.state.save(target, &state).await?;
343
344 tracing::debug!("sync reported no change");
345 self.publish_completed(target, &sync_id, 0, now);
346 Ok(SyncOutcome::NotModified)
347 }
348
349 Err(error) => {
350 let outcome = SyncOutcome::Failed {
351 kind: error.kind(),
352 message: error.to_string(),
353 };
354 state.record(now, outcome.clone());
355 self.state.save(target, &state).await?;
356
357 let retry_at = Some(self.due_at_for(&state, &policy));
358 tracing::warn!(kind = ?error.kind(), %error, ?retry_at, "sync failed");
359
360 let _ = self.events.publish(PlatformEvent::SyncFailed(SyncFailed {
361 sync: sync_id,
362 connector: target.connector.clone(),
363 account: target.account.clone(),
364 kind: error.kind(),
365 message: error.to_string(),
366 retry_at,
367 }));
368
369 Err(error)
370 }
371 }
372 }
373
374 pub async fn health(&self) -> Health {
376 let now = self.clock.now();
377 let targets: Vec<(SyncTarget, SyncPolicy)> = self
378 .read()
379 .iter()
380 .map(|(target, registration)| (target.clone(), registration.policy))
381 .collect();
382
383 let mut states = Vec::new();
384 for (target, policy) in targets {
385 let state = self.state.load(&target).await.unwrap_or_default();
386 states.push(health_of(&state, &policy, now));
387 }
388
389 Health::aggregate(states)
390 }
391
392 pub async fn health_of_account(&self, connector: &ConnectorId, account: &AccountId) -> Health {
394 let now = self.clock.now();
395 let targets: Vec<(SyncTarget, SyncPolicy)> = self
396 .read()
397 .iter()
398 .filter(|(target, _)| &target.connector == connector && &target.account == account)
399 .map(|(target, registration)| (target.clone(), registration.policy))
400 .collect();
401
402 let mut states = Vec::new();
403 for (target, policy) in targets {
404 let state = self.state.load(&target).await.unwrap_or_default();
405 states.push(health_of(&state, &policy, now));
406 }
407
408 Health::aggregate(states)
409 }
410
411 pub async fn run(&self, stop: CancellationToken) {
421 tracing::debug!("sync scheduler started");
422 let mut ticker = tokio::time::interval(TICK);
423
424 loop {
425 tokio::select! {
426 _ = stop.cancelled() => break,
427 _ = ticker.tick() => {
428 self.run_due(self.clock.now()).await;
429 }
430 }
431 }
432
433 tracing::debug!("sync scheduler stopped");
434 }
435
436 fn publish_completed(
437 &self,
438 target: &SyncTarget,
439 sync_id: &SyncId,
440 changed: u64,
441 at: OffsetDateTime,
442 ) {
443 let _ = self
444 .events
445 .publish(PlatformEvent::SyncCompleted(SyncCompleted {
446 sync: sync_id.clone(),
447 connector: target.connector.clone(),
448 account: target.account.clone(),
449 changed,
450 at,
451 }));
452 }
453
454 fn next_random(&self) -> f64 {
456 let previous = self
457 .seed
458 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |seed| {
459 Some(
460 seed.wrapping_mul(6_364_136_223_846_793_005)
461 .wrapping_add(1_442_695_040_888_963_407),
462 )
463 });
464
465 let value = previous
466 .unwrap_or(0)
467 .wrapping_mul(6_364_136_223_846_793_005);
468 f64::from((value >> 40) as u32) / f64::from(1u32 << 24)
469 }
470}
471
472impl SyncEngine {
474 pub fn register_every(
475 &self,
476 target: SyncTarget,
477 interval: Duration,
478 source: Arc<dyn SyncSource>,
479 ) {
480 self.register(target, SyncPolicy::every(interval), source);
481 }
482}