Просмотр исходного кода

bin/app/src/plugin/darkirc: implement reverse syncing; newest dag first and other dags sync as a background task

x 2 дней назад
Родитель
Сommit
93739d88e2
1 измененных файлов с 111 добавлено и 15 удалено
  1. 111 15
      bin/app/src/plugin/darkirc.rs

+ 111 - 15
bin/app/src/plugin/darkirc.rs

@@ -19,7 +19,7 @@
 use std::{
 use std::{
     collections::{HashMap, HashSet},
     collections::{HashMap, HashSet},
     io::Cursor,
     io::Cursor,
-    sync::{Arc, OnceLock, Weak},
+    sync::{atomic::Ordering, Arc, OnceLock, Weak},
     time::UNIX_EPOCH,
     time::UNIX_EPOCH,
 };
 };
 
 
@@ -74,6 +74,11 @@ const COOLOFF_SLEEP_TIME: u64 = 20;
 const COOLOFF_SYNC_ATTEMPTS: usize = 6;
 const COOLOFF_SYNC_ATTEMPTS: usize = 6;
 const SYNC_MIN_PEERS: usize = 2;
 const SYNC_MIN_PEERS: usize = 2;
 
 
+/// Event graph rotation: `max_dags` hourly slots (`hours_rotation: 1`).
+const DAGS_COUNT: u64 = 24;
+/// Milliseconds in one rotation period (1 hour).
+const HOUR_MS: u64 = 3_600_000;
+
 pub(crate) const P2P_OUTBOUND_ACTIVE: usize = 6;
 pub(crate) const P2P_OUTBOUND_ACTIVE: usize = 6;
 const P2P_OUTBOUND_SLEEP: usize = 1;
 const P2P_OUTBOUND_SLEEP: usize = 1;
 
 
@@ -366,7 +371,7 @@ impl DarkIrc {
         let mut sync_attempt = 0;
         let mut sync_attempt = 0;
         // TODO: these should be configurable
         // TODO: these should be configurable
         let fast_mode = false;
         let fast_mode = false;
-        let dags_count = 24;
+        let mut newest_synced = false;
         loop {
         loop {
             if self.p2p.is_connected() {
             if self.p2p.is_connected() {
                 let peers_count = self.p2p.peers_count();
                 let peers_count = self.p2p.peers_count();
@@ -399,29 +404,26 @@ impl DarkIrc {
                         break
                         break
                     }
                     }
                 }
                 }
-                i!("Syncing event DAG (attempt #{sync_attempt})");
-                // Sync mode is now per-call: full sync replays
-                // every event (heavy, used by archival nodes), fast
-                // sync only fetches headers (light, used by clients
-                // that don't need to re-verify history).
-                let sync_result = if fast_mode {
-                    self.event_graph.sync_selected_headers(dags_count).await
-                } else {
-                    self.event_graph.sync_selected(dags_count).await
-                };
+                // Sync only the newest DAG first. Older DAGs are caught up in
+                // the background by `catch_up_sync`, so the `synced` flag (and
+                // the `connect` notification) is reached after the first DAG
+                // instead of after all `DAGS_COUNT` of them.
+                let latest_ts = self.event_graph.current_genesis.read().await.header.timestamp;
+                i!("Syncing newest event DAG ({latest_ts}) (attempt #{sync_attempt})");
+                let sync_result = self.sync_dag_slot(latest_ts, fast_mode).await;
                 match sync_result {
                 match sync_result {
                     Ok(()) => {
                     Ok(()) => {
                         i!(
                         i!(
-                            "Event DAG synced successfully ({} mode, {} dag(s))",
+                            "Newest event DAG synced successfully ({} mode)",
                             if fast_mode { "fast" } else { "full" },
                             if fast_mode { "fast" } else { "full" },
-                            dags_count,
                         );
                         );
+                        newest_synced = true;
                         break
                         break
                     }
                     }
                     Err(e) => {
                     Err(e) => {
                         // TODO: Maybe at this point we should prune or something?
                         // TODO: Maybe at this point we should prune or something?
                         // TODO: Or maybe just tell the user to delete the DAG from FS.
                         // TODO: Or maybe just tell the user to delete the DAG from FS.
-                        e!("Failed syncing DAG ({e}), retrying...");
+                        e!("Failed syncing newest DAG ({e}), retrying...");
                     }
                     }
                 }
                 }
             } else {
             } else {
@@ -430,6 +432,19 @@ impl DarkIrc {
             }
             }
         }
         }
 
 
+        // The newest DAG is synced: flip the `synced` flag so live `EventPut`
+        // ingestion and the netstatus UI unblock, then let the remaining older
+        // DAGs catch up in the background.
+        if newest_synced {
+            self.event_graph.synced.store(true, Ordering::Release);
+            i!("Marked event graph as synced (newest DAG done)");
+
+            let self_ = self.clone();
+            let catch_up_task =
+                self.ex.clone().spawn(async move { self_.catch_up_sync(fast_mode).await });
+            self.tasks.lock().push(catch_up_task);
+        }
+
         let peers_count = self.p2p.peers_count();
         let peers_count = self.p2p.peers_count();
         self.notify_connect(peers_count, self.event_graph.is_synced()).await;
         self.notify_connect(peers_count, self.event_graph.is_synced()).await;
 
 
@@ -446,6 +461,87 @@ impl DarkIrc {
         }
         }
     }
     }
 
 
+    /// Sync a single DAG slot, choosing `dag_sync_headers` (fast) or `dag_sync`
+    /// (full) per the per-call `fast_mode` flag. Shared by the foreground
+    /// newest-DAG phase and the background catch-up phase.
+    async fn sync_dag_slot(&self, dag_ts: u64, fast_mode: bool) -> DarkFiResult<()> {
+        if fast_mode {
+            self.event_graph.dag_sync_headers(dag_ts).await
+        } else {
+            self.event_graph.dag_sync(dag_ts).await
+        }
+    }
+
+    /// Background catch-up phase: after the newest DAG has synced (phase 1),
+    /// walk the remaining DAG slots newest-to-oldest and sync each one.
+    ///
+    /// Per-slot failures are retried across rounds with `COOLOFF_SLEEP_TIME`
+    /// pauses (no give-up), while the walk keeps making progress on the other
+    /// pending slots. `current_genesis` is re-read before every retry round so
+    /// rotation mid-catch-up is handled: a newer slot that rotates in is synced
+    /// first, and a pending slot that rotates out of the retention window is
+    /// dropped from the walk.
+    async fn catch_up_sync(self: Arc<Self>, fast_mode: bool) {
+        i!("Starting background catch-up of older DAGs (newest-to-oldest)");
+
+        // The newest slot is already synced by phase 1; collect the rest of the
+        // retention window below it.
+        let mut last_newest = self.event_graph.current_genesis.read().await.header.timestamp;
+        let mut pending: Vec<u64> =
+            (1..DAGS_COUNT).map(|i| last_newest.saturating_sub(i * HOUR_MS)).collect();
+
+        loop {
+            // Re-read `current_genesis` before each retry round: rotation may
+            // have shifted the newest slot or dropped a pending slot.
+            let latest_ts = self.event_graph.current_genesis.read().await.header.timestamp;
+
+            // A newer DAG rotated in: it is the new newest slot and must sync
+            // before the remaining older pending DAGs.
+            if latest_ts > last_newest {
+                i!("Syncing newly rotated-in DAG ({latest_ts})");
+                loop {
+                    match self.sync_dag_slot(latest_ts, fast_mode).await {
+                        Ok(()) => break,
+                        Err(e) => {
+                            e!("Failed syncing newly rotated-in DAG ({latest_ts}): {e}, cooling off");
+                            sleep(COOLOFF_SLEEP_TIME).await;
+                        }
+                    }
+                }
+                last_newest = latest_ts;
+                pending = (1..DAGS_COUNT).map(|i| latest_ts.saturating_sub(i * HOUR_MS)).collect();
+            }
+
+            // Clamp the walk to the retention window: slots rotated out fall
+            // below the horizon and are skipped rather than retried forever.
+            let horizon = latest_ts.saturating_sub((DAGS_COUNT - 1) * HOUR_MS);
+            let mut still_pending = Vec::new();
+            for dag_ts in pending.iter().copied() {
+                if dag_ts < horizon {
+                    i!("Skipping rotated-out DAG ({dag_ts})");
+                    continue
+                }
+
+                match self.sync_dag_slot(dag_ts, fast_mode).await {
+                    Ok(()) => {
+                        i!("Synced older DAG ({dag_ts})");
+                    }
+                    Err(e) => {
+                        e!("Failed syncing older DAG ({dag_ts}): {e}, will retry after cooldown");
+                        still_pending.push(dag_ts);
+                        sleep(COOLOFF_SLEEP_TIME).await;
+                    }
+                }
+            }
+
+            pending = still_pending;
+            if pending.is_empty() {
+                i!("Background catch-up complete; all older DAGs synced");
+                break
+            }
+        }
+    }
+
     /// Send a notification when there's a change in number of peers or the DAG sync status
     /// Send a notification when there's a change in number of peers or the DAG sync status
     pub async fn notify_connect(&self, peers_count: usize, is_dag_synced: bool) {
     pub async fn notify_connect(&self, peers_count: usize, is_dag_synced: bool) {
         let node = self.node.upgrade().unwrap();
         let node = self.node.upgrade().unwrap();