Skip to main content

coven_replication/sync/sync_loop/
thread.rs

1use futures_util::FutureExt;
2use std::sync::atomic::{AtomicBool, Ordering};
3use std::sync::{Arc, Condvar, Mutex};
4use std::time::Duration;
5
6use tracing::{debug, error, info};
7
8use super::{BlockedOperation, SyncCommand, SyncLoopFailure, SyncLoopHandleInner, SyncLoopStatus};
9use crate::sync::loop_policy::{self, LoopWait, SyncLoopReport, SyncLoopSuccess};
10use coven_foundation::stage_timing::StageTimings;
11
12struct RuntimeSlotState {
13    loop_thread: Option<SyncLoopThread>,
14    cancelled: bool,
15}
16
17struct RuntimeSlot {
18    state: Mutex<RuntimeSlotState>,
19    changed: Condvar,
20}
21
22impl RuntimeSlot {
23    fn new() -> Self {
24        Self {
25            state: Mutex::new(RuntimeSlotState {
26                loop_thread: None,
27                cancelled: false,
28            }),
29            changed: Condvar::new(),
30        }
31    }
32
33    fn install(&self, loop_thread: SyncLoopThread) {
34        let mut state = self.state.lock().unwrap_or_else(|error| error.into_inner());
35        state.loop_thread = Some(loop_thread);
36        self.changed.notify_one();
37    }
38
39    fn cancel(&self) {
40        let mut state = self.state.lock().unwrap_or_else(|error| error.into_inner());
41        state.cancelled = true;
42        self.changed.notify_one();
43    }
44
45    fn run(&self, runtime: tokio::runtime::Runtime) {
46        let mut state = self.state.lock().unwrap_or_else(|error| error.into_inner());
47        while state.loop_thread.is_none() && !state.cancelled {
48            state = self
49                .changed
50                .wait(state)
51                .unwrap_or_else(|error| error.into_inner());
52        }
53        let loop_thread = state.loop_thread.take();
54        drop(state);
55        if let Some(loop_thread) = loop_thread {
56            loop_thread.run(runtime);
57        }
58    }
59}
60
61/// A sync-loop OS thread whose Tokio runtime is ready but has not received a
62/// Store session yet.
63///
64/// Setup creates this before committing credentials or initializing the Store.
65/// Attaching the initialized session is then an in-memory handoff with no
66/// remaining thread or runtime construction that can fail.
67pub struct PreparedSyncLoopRuntime {
68    slot: Arc<RuntimeSlot>,
69    thread_handle: Option<std::thread::JoinHandle<()>>,
70}
71
72impl PreparedSyncLoopRuntime {
73    pub(super) fn prepare() -> Result<Self, super::SyncLoopError> {
74        let slot = Arc::new(RuntimeSlot::new());
75        let thread_slot = Arc::clone(&slot);
76        let (ready_tx, ready_rx) = std::sync::mpsc::sync_channel(1);
77        let thread_handle = std::thread::Builder::new()
78            .name("coven-sync-loop".to_string())
79            .stack_size(8 * 1024 * 1024)
80            .spawn(move || {
81                let runtime = match tokio::runtime::Builder::new_current_thread()
82                    .enable_all()
83                    .build()
84                {
85                    Ok(runtime) => runtime,
86                    Err(error) => {
87                        let _ = ready_tx.send(Err(Arc::new(error)));
88                        return;
89                    }
90                };
91                if ready_tx.send(Ok(())).is_err() {
92                    return;
93                }
94                thread_slot.run(runtime);
95            })
96            .map_err(super::SyncLoopError::ThreadSpawn)?;
97
98        match ready_rx.recv() {
99            Ok(Ok(())) => Ok(Self {
100                slot,
101                thread_handle: Some(thread_handle),
102            }),
103            Ok(Err(error)) => {
104                let _ = thread_handle.join();
105                Err(super::SyncLoopError::Runtime(error))
106            }
107            Err(_) => {
108                let _ = thread_handle.join();
109                Err(super::SyncLoopError::ThreadPanicked)
110            }
111        }
112    }
113
114    pub(super) fn install(mut self, loop_thread: SyncLoopThread) -> std::thread::JoinHandle<()> {
115        self.slot.install(loop_thread);
116        self.thread_handle
117            .take()
118            .expect("prepared sync runtime owns its thread until installation")
119    }
120}
121
122impl Drop for PreparedSyncLoopRuntime {
123    fn drop(&mut self) {
124        let Some(thread_handle) = self.thread_handle.take() else {
125            return;
126        };
127        self.slot.cancel();
128        let _ = thread_handle.join();
129    }
130}
131
132pub(super) struct SyncLoopThread {
133    inner: Arc<SyncLoopHandleInner>,
134    trigger_rx: tokio::sync::mpsc::Receiver<()>,
135    command_rx: tokio::sync::mpsc::Receiver<SyncCommand>,
136    stop_rx: tokio::sync::watch::Receiver<bool>,
137    eager_cache_cancel_rx: tokio::sync::watch::Receiver<bool>,
138    activate_rx: tokio::sync::watch::Receiver<bool>,
139    status_tx: tokio::sync::watch::Sender<SyncLoopStatus>,
140    eager_cache_status_tx: tokio::sync::watch::Sender<crate::sync::store::EagerCacheFillStatus>,
141    running: Arc<AtomicBool>,
142}
143
144impl SyncLoopThread {
145    pub(super) fn new(
146        inner: Arc<SyncLoopHandleInner>,
147        trigger_rx: tokio::sync::mpsc::Receiver<()>,
148        command_rx: tokio::sync::mpsc::Receiver<SyncCommand>,
149        stop_rx: tokio::sync::watch::Receiver<bool>,
150        eager_cache_cancel_rx: tokio::sync::watch::Receiver<bool>,
151        activate_rx: tokio::sync::watch::Receiver<bool>,
152        status_tx: tokio::sync::watch::Sender<SyncLoopStatus>,
153        eager_cache_status_tx: tokio::sync::watch::Sender<crate::sync::store::EagerCacheFillStatus>,
154        running: Arc<AtomicBool>,
155    ) -> Self {
156        Self {
157            inner,
158            trigger_rx,
159            command_rx,
160            stop_rx,
161            eager_cache_cancel_rx,
162            activate_rx,
163            status_tx,
164            eager_cache_status_tx,
165            running,
166        }
167    }
168
169    fn run(mut self, runtime: tokio::runtime::Runtime) {
170        let _running_guard = RunningGuard {
171            running: Arc::clone(&self.running),
172        };
173        let status_tx = self.status_tx.clone();
174        if runtime
175            .block_on(std::panic::AssertUnwindSafe(self.run_loop()).catch_unwind())
176            .is_err()
177        {
178            let failure = SyncLoopFailure::Panicked;
179            error!("{failure}");
180            status_tx.send_replace(SyncLoopStatus::Failed { error: failure });
181            let cancelled = match self.eager_cache_status_tx.borrow().clone() {
182                crate::sync::store::EagerCacheFillStatus::Scanning => {
183                    Some(crate::sync::store::EagerCacheFillStatus::Cancelled(
184                        crate::sync::store::EagerCacheFillProgress::empty(),
185                    ))
186                }
187                crate::sync::store::EagerCacheFillStatus::Downloading(progress) => Some(
188                    crate::sync::store::EagerCacheFillStatus::Cancelled(progress),
189                ),
190                _ => None,
191            };
192            if let Some(cancelled) = cancelled {
193                self.eager_cache_status_tx.send_replace(cancelled);
194            }
195        }
196    }
197
198    async fn run_loop(&mut self) {
199        if !self.wait_for_activation().await {
200            return;
201        }
202
203        let eager_components = Arc::clone(&self.inner);
204        let mut eager_cancel = self.eager_cache_cancel_rx.clone();
205        // Raised when the cycle loop ends, so a fill parked between passes stops
206        // with it rather than holding the loop's thread open.
207        let cycles_ended = Arc::new(tokio::sync::Notify::new());
208        let eager_cycles_ended = Arc::clone(&cycles_ended);
209        let eager_status = self.eager_cache_status_tx.clone();
210        // The first pass covers whatever the database already holds; each pass
211        // after it waits for a cycle to materialize rows and scans again,
212        // because a pull records what its rows bind and downloads none of it.
213        // A cancelled fill stays cancelled — the host asked for it to stop, not
214        // to pause.
215        let eager_fill = async move {
216            loop {
217                if let Err(error) = eager_components
218                    .components
219                    .fill_eager_cache(eager_cancel.clone(), &eager_status)
220                    .await
221                {
222                    error!(%error, "post-open eager cache fill failed");
223                    return;
224                }
225                if *eager_cancel.borrow() {
226                    return;
227                }
228                tokio::select! {
229                    () = eager_components.components.eager_fill_wanted().notified() => {}
230                    () = eager_cycles_ended.notified() => return,
231                    changed = eager_cancel.changed() => {
232                        if changed.is_err() || *eager_cancel.borrow() {
233                            return;
234                        }
235                    }
236                }
237            }
238        };
239        tokio::pin!(eager_fill);
240        let cycles = self.run_cycles();
241        tokio::pin!(cycles);
242        tokio::select! {
243            () = &mut eager_fill => cycles.await,
244            () = &mut cycles => {
245                // An in-flight fill still observes the cancellation the stop
246                // raised and reports where it stopped; a parked one ends here.
247                cycles_ended.notify_one();
248                eager_fill.await;
249            }
250        }
251    }
252
253    async fn run_cycles(&mut self) {
254        if !self.wait_for_first_cycle().await {
255            return;
256        }
257
258        let mut consecutive_failures = 0;
259        while self.running.load(Ordering::Acquire) && !*self.stop_rx.borrow() {
260            // Everything between two idle waits, so the gap between cycles is
261            // accounted for even when the cycle itself was not the slow part.
262            let mut timings = StageTimings::counting(
263                "sync loop iteration",
264                self.inner.components.provider_requests(),
265            );
266            self.status_tx.send_replace(SyncLoopStatus::CheckingStorage);
267            let reachable = timings
268                .stage("probe storage", self.inner.components.probe_storage())
269                .await;
270            let (decision, status) = match reachable {
271                Err(error) => {
272                    let error = Arc::new(error);
273                    let status = storage_check_failure_status(Arc::clone(&error));
274                    let failure = SyncLoopFailure::Storage(error);
275                    (
276                        loop_policy::after_failure(failure, consecutive_failures, 300),
277                        status,
278                    )
279                }
280                Ok(_) => {
281                    self.status_tx.send_replace(SyncLoopStatus::Publishing);
282                    self.run_reachable_cycle(consecutive_failures, &mut timings)
283                        .await
284                }
285            };
286            timings.report();
287            consecutive_failures = decision.consecutive_failures;
288            self.status_tx.send_replace(status);
289            if !self
290                .wait_for_next_cycle(decision.wait, consecutive_failures)
291                .await
292            {
293                break;
294            }
295        }
296    }
297
298    async fn wait_for_activation(&mut self) -> bool {
299        loop {
300            if *self.activate_rx.borrow() {
301                return true;
302            }
303            tokio::select! {
304                changed = self.activate_rx.changed() => {
305                    if changed.is_err() {
306                        return false;
307                    }
308                }
309                changed = self.stop_rx.changed() => {
310                    if changed.is_err() || *self.stop_rx.borrow() {
311                        info!("Prepared sync loop stopped before activation");
312                        return false;
313                    }
314                }
315            }
316        }
317    }
318
319    async fn wait_for_first_cycle(&mut self) -> bool {
320        let startup_delay = tokio::time::sleep(Duration::from_secs(3));
321        tokio::pin!(startup_delay);
322        loop {
323            tokio::select! {
324                _ = &mut startup_delay => return true,
325                changed = self.stop_rx.changed() => {
326                    if changed.is_err() || *self.stop_rx.borrow() {
327                        info!("Sync loop stopped before first cycle");
328                        return false;
329                    }
330                }
331                message = self.trigger_rx.recv() => {
332                    if message.is_none() {
333                        info!("Sync trigger channel closed before first cycle");
334                        return false;
335                    }
336                    return true;
337                }
338                command = self.command_rx.recv() => {
339                    let Some(command) = command else {
340                        info!("Sync command channel closed before first cycle");
341                        return false;
342                    };
343                    self.inner.execute_command(command).await;
344                }
345            }
346        }
347    }
348
349    async fn run_reachable_cycle(
350        &self,
351        consecutive_failures: u32,
352        timings: &mut StageTimings,
353    ) -> (loop_policy::SyncLoopDecision, SyncLoopStatus) {
354        let (decision, cycle_went_offline) =
355            match timings.stage("cycle", self.inner.run_single_cycle()).await {
356                Ok(result) => (loop_policy::after_success(result), false),
357                Err(error) => {
358                    let offline = error.is_offline();
359                    let failure = SyncLoopFailure::Cycle(Arc::new(error));
360                    (
361                        loop_policy::after_failure(failure, consecutive_failures, 300),
362                        offline,
363                    )
364                }
365            };
366        let status = match &decision.report {
367            SyncLoopReport::Success(success) => {
368                match timings
369                    .stage(
370                        "read blocked operations",
371                        self.inner.components.blocked_operations(),
372                    )
373                    .await
374                {
375                    Ok(operations) => current_success_status(operations, success.clone()),
376                    Err(error) => SyncLoopStatus::Failed {
377                        error: SyncLoopFailure::BlockedOperations(Arc::new(error)),
378                    },
379                }
380            }
381            SyncLoopReport::Failure(_) if cycle_went_offline => SyncLoopStatus::Offline,
382            SyncLoopReport::Failure(error) => SyncLoopStatus::Failed {
383                error: error.clone(),
384            },
385        };
386        (decision, status)
387    }
388
389    async fn wait_for_next_cycle(&mut self, wait: LoopWait, consecutive_failures: u32) -> bool {
390        let duration = match wait {
391            LoopWait::Immediate => Duration::ZERO,
392            LoopWait::Idle => Duration::from_secs(crate::sync::backoff::backoff_secs(0, 300)),
393            LoopWait::BackoffSecs(secs) => Duration::from_secs(secs),
394        };
395        if matches!(wait, LoopWait::BackoffSecs(_)) {
396            debug!("Backing off {duration:?} after {consecutive_failures} consecutive failure(s)");
397        }
398        tokio::select! {
399            _ = tokio::time::sleep(duration) => true,
400            changed = self.stop_rx.changed() => {
401                if changed.is_err() || *self.stop_rx.borrow() {
402                    info!("Sync loop stop requested");
403                    false
404                } else {
405                    true
406                }
407            }
408            message = self.trigger_rx.recv() => {
409                if message.is_none() {
410                    info!("Sync trigger channel closed, stopping sync loop");
411                    false
412                } else {
413                    true
414                }
415            }
416            command = self.command_rx.recv() => {
417                let Some(command) = command else {
418                    info!("Sync command channel closed, stopping sync loop");
419                    return false;
420                };
421                self.inner.execute_command(command).await;
422                true
423            }
424        }
425    }
426}
427
428pub(super) fn storage_check_failure_status(
429    error: Arc<coven_protocol::objects::StorageError>,
430) -> SyncLoopStatus {
431    if error.is_transport() {
432        SyncLoopStatus::Offline
433    } else {
434        SyncLoopStatus::Failed {
435            error: SyncLoopFailure::Storage(error),
436        }
437    }
438}
439
440/// The terminal status of a cycle that reached storage: `Blocked` whenever any
441/// durable operation is waiting on a person, `Synchronized` only when none is.
442pub(crate) fn current_success_status(
443    operations: Vec<BlockedOperation>,
444    success: SyncLoopSuccess,
445) -> SyncLoopStatus {
446    if operations.is_empty() {
447        SyncLoopStatus::Synchronized(success)
448    } else {
449        SyncLoopStatus::Blocked {
450            success,
451            operations,
452        }
453    }
454}
455
456struct RunningGuard {
457    running: Arc<AtomicBool>,
458}
459
460impl Drop for RunningGuard {
461    fn drop(&mut self) {
462        self.running.store(false, Ordering::Release);
463    }
464}