Repository: ChainSafe/forest
Pull request #5639: feat(gc): automatic scheduler for snapshot GC
Author: login: hanabi1224
Created: 2025-05-09T09:45:50Z | Merged: 2025-05-22T21:20:45Z

## Summary of changes

<!-- Please write a comprehensive summary of your changes and what was the motivation behind them -->

Changes introduced in this pull request:

-

## Reference issue to close (if applicable)

<!-- Include the issue reference this pull request is connected to -->
<!-- See more keywords here https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword -->
<!--(e.g. Closes #1)-->

Closes

## Other information and links

<!-- Add any other context about the pull request here. Those might be helpful links based on your investigation, relevant commits from this or other repositories or anything else -->

## Change checklist

<!-- Please add a changelog entry for your change if needed. -->
<!-- Follow this format https://keepachangelog.com/en/1.0.0/ -->

- [x] I have performed a self-review of my own code,
- [x] I have made corresponding changes to the documentation. All new code adheres to the team's [documentation standards](https://github.com/ChainSafe/forest/wiki/Documentation-practices),
- [x] I have added tests that prove my fix is effective or that my feature works (if possible),
- [x] I have made sure the [CHANGELOG][1] is up-to-date. All user-facing changes should be reflected in this document.

<!-- Thank you 🔥 -->

[1]: https://github.com/ChainSafe/forest/blob/main/CHANGELOG.md



Diff:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 807945672a0e..7654b6748dc6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -33,6 +33,8 @@
 
 -[#5629](https://github.com/ChainSafe/forest/pull/5629) Save default RPC token and consume it automatically.
 
+-[#5639](https://github.com/ChainSafe/forest/pull/5639) Add automatic scheduler for snapshot GC.
+
 ### Changed
 
 -[#5616](https://github.com/ChainSafe/forest/pull/5616) Remove the initial background task for populating Ethereum mappings. Use `forest-tool index backfill` to perform this operation offline instead.
diff --git a/docs/docs/users/guides/gc.md b/docs/docs/users/guides/gc.md
index fb61423d4c22..7000b975d9bc 100644
--- a/docs/docs/users/guides/gc.md
+++ b/docs/docs/users/guides/gc.md
@@ -3,21 +3,27 @@ title: Garbage Collector
 sidebar_position: 5
 ---
 
-### Enabling/Disabling Garbage Collection
+### Enabling/Disabling Automatic Garbage Collection
 
-By default, garbage collection is automatically enabled in Forest to ensure that unnecessary data is regularly cleared out, optimizing disk usage and performance.
+By default, automatic garbage collection is enabled in Forest to ensure that unnecessary data is regularly cleared out, optimizing disk usage and performance. By default, it runs every 7 days (20160 epochs). The interval can be overridden by setting environment variable `FOREST_SNAPSHOT_GC_INTERVAL_EPOCHS`
 
-If you want to disable GC, for example, while testing new features or running performance benchmarks where GC may cause unnecessary overhead, you can do so by starting the Forest daemon with the `--no-gc` flag.
+If you want to disable the automatic GC, for example, while testing new features or running performance benchmarks where GC may cause unnecessary overhead, you can do so by starting the Forest daemon with the `--no-gc` flag.
 
-This ensures that GC process is skipped, preventing potential performance impact.
+This ensures that GC scheduler is disabled, preventing potential performance impact.
+
+### Manually Run Garbage Collection
+
+GC can be trigger manually with `forest-cli chain prune snap`, regardless whether GC scheduler is enabled or disabled. Note that there is a global lock that ensures only one GC task could be running.
 
 ### Cadence of GC Runs
 
 Garbage Collection (GC) runs on a regular schedule and follows these steps:
 
-- **Mark**: Scan all the data blocks, create a unique identifier (hash) for each, and store these in a list.
-- Wait for the chain to reach finality
-- **Sweep**: Check which data is still needed by starting at the latest block. Anything not needed and older than the finality point gets deleted.
+- Export an effective standard lite snapshot in `.forest.car.zst` format.
+- Stop the node.
+- Purge parity-db columns that serve as non-persistent blockstore.
+- Purge old CAR database files.
+- Restart the node.
 
 This process keeps the system clean by regularly removing old, unused data.
 
@@ -25,13 +31,14 @@ This process keeps the system clean by regularly removing old, unused data.
 
 The GC process removes unreachable blocks and state trees that are older than chain finality epochs. Specifically, it:
 
-- Marks blocks that can no longer be reached from the blockchain’s HEAD and prepares them for deletion.
 - Removes state trees that are no longer needed to ensure the persistence layer remains lightweight and efficient.
+- Removes stale lite snapshots in the CAR database.
 
 GC does not remove:
 
-- Any block that is reachable from the current heaviest tipset.
-- Data younger than chain finality epochs (to prevent the accidental removal of blocks that may later be part of a fork).
+- Persistent blockstore column.
+- Settings column.
+- Ethereum index column
 
 ### When Should You Disable GC?
 
@@ -44,41 +51,36 @@ Always remember to enable GC when moving back to production or long-term testing
 
 ## What Happens During a GC Run?
 
-### Memory Usage Spikes
+### RAM/Disk Usage Spikes
 
-During the GC process, Forest consumes extra memory temporarily:
+During the GC process, Forest consumes extra RAM and disk space temporarily:
 
-- **Mark Phase**: It requires 4 bytes of memory per database record.
-- **Filter Phase**: While traversing reachable blocks, it uses 32 bytes of memory per reachable block.
+- While traversing reachable blocks, it uses 32 bytes of RAM per reachable block.
+- While exporting a lite snapshot, it uses extra disk space before cleaning up parity-db and stale CAR snapshots.
 
-For a typical 100 GiB mainnet snapshot, this results in approximately 2.5 GiB of additional memory usage.
+For a typical ~80 GiB mainnet snapshot, this results in ~2.5 GiB of additional RAM and ~80 GiB disk space usage.
 
 ### Syncing Pauses or Performance Overheads
 
-While GC runs in the background, it can cause some delays or pauses, particularly during the "filter" stage, where reachable blocks are processed:
+While GC runs in the background, it can cause some delays or pauses, particularly during the "export" stage, where reachable blocks are processed:
 
 - **Syncing Pauses**: There may be brief interruptions in syncing as resources are allocated for the GC process.
-- **Performance Overhead**: While relatively efficient, the mark-and-sweep algorithm could slow down operations slightly, especially on large datasets.
+- **Performance Overhead**: While relatively efficient, the chain traversal algorithm could slow down operations slightly.
+- **Reboot pauses**: The GC stops the node before cleaning up parity-db and CAR snapshots and then restarts the node, which could take `~10s-~30s` on mainnet
 
 ## Disk Usage
 
 ### With GC
 
-When GC is enabled, you can expect disk usage to be slightly higher than live data for three reasons:
-
-1. Unreachable data isn’t removed until it’s at least 7.5 hours old.
-2. The GC is conservative, leaving behind a small amount of unreachable data (less than 1%).
-3. Some fragmentation in the blockstore backend may prevent immediate disk space reclamation.
-
-Overall, the disk usage is expected to be slightly above the live dataset size, which helps maintain optimal database performance.
+When GC is running, it consumes extra disk space of the size of a standard lite snapshot:
 
 ### Without GC
 
-If you disable GC, the database will continue to grow as unreachable data remains in the system. Over time, this can lead to significant disk space consumption, especially in active environments where many blocks are added, and forks may create unreachable blocks.
+If GC is disabled, the database will continue to grow as unreachable data remains in the system. Over time, this can lead to significant disk space consumption, especially in active environments where many blocks are added, and forks may create unreachable blocks.
 
 Without GC, expect disk usage to grow without bounds as old data is never purged. This could lead to:
 
 - Higher storage costs.
 - Decreased performance as the database grows larger and becomes more fragmented.
 
-For detailed information on the inner workings of the GC, refer to the [GC documentation](https://docs.rs/forest-filecoin/0.20.0/forest_filecoin/db/gc/index.html)
+For detailed information on the inner workings of the GC, refer to the [GC documentation](https://docs.rs/forest-filecoin/latest/forest/db/gc/index.html)
diff --git a/docs/docs/users/reference/env_variables.md b/docs/docs/users/reference/env_variables.md
index 78348264bff0..7efabaed24d8 100644
--- a/docs/docs/users/reference/env_variables.md
+++ b/docs/docs/users/reference/env_variables.md
@@ -48,6 +48,8 @@ process.
 | `FOREST_CHAIN_INDEXER_ENABLED`                            | 1 or true                        | false                                          | 1                                                             | Whether or not to index the chain to support the Ethereum RPC API                                                     |
 | `FOREST_MESSAGES_IN_TIPSET_CACHE_SIZE`                    | positive integer                 | 100                                            | 42                                                            | The size of an internal cache of tipsets to messages                                                                  |
 | `FOREST_STATE_MIGRATION_DB_WRITE_BUFFER`                  | non-negative integer             | 10000                                          | 100000                                                        | The size of db write buffer for state migration (`~10MB` RAM per `10k` buffer)                                        |
+| `FOREST_SNAPSHOT_GC_INTERVAL_EPOCHS`                      | non-negative integer             | 20160                                          | 8000                                                          | The interval in epochs for scheduling snapshot GC                                                                     |
+| `FOREST_SNAPSHOT_GC_CHECK_INTERVAL_SECONDS`               | non-negative integer             | 300                                            | 60                                                            | The interval in seconds for checking if snapshot GC should run                                                        |
 
 ### `FOREST_F3_SIDECAR_FFI_BUILD_OPT_OUT`
 
diff --git a/src/daemon/mod.rs b/src/daemon/mod.rs
index 0a6455b53c3a..79c4c4798ec0 100644
--- a/src/daemon/mod.rs
+++ b/src/daemon/mod.rs
@@ -590,8 +590,14 @@ pub(super) async fn start(
         .context("failed to set GLOBAL_SNAPSHOT_GC")?;
     tokio::task::spawn({
         let snap_gc = snap_gc.clone();
-        async move { snap_gc.event_toop().await }
+        async move { snap_gc.event_loop().await }
     });
+    if !opts.no_gc {
+        tokio::task::spawn({
+            let snap_gc = snap_gc.clone();
+            async move { snap_gc.scheduler_loop().await }
+        });
+    }
     loop {
         tokio::select! {
             _ = snap_gc_reboot_rx.recv_async() => {
@@ -599,6 +605,7 @@ pub(super) async fn start(
             }
             result = start_services(start_time, &opts, config.clone(), shutdown_send.clone(), |ctx| {
                 snap_gc.set_db(ctx.db.clone());
+                snap_gc.set_car_db_head_epoch(ctx.db.heaviest_tipset().map(|ts|ts.epoch()).unwrap_or_default());
             }) => {
                 break result
             }
diff --git a/src/db/gc/snapshot.rs b/src/db/gc/snapshot.rs
index 91a9b3f1f95c..ece9fe40346b 100644
--- a/src/db/gc/snapshot.rs
+++ b/src/db/gc/snapshot.rs
@@ -10,11 +10,11 @@
 //! 2000 epochs of most recent state-trees and messages.
 //!
 //! ## GC Workflow
-//! 1. Exports an effective standard lite snapshot in `.forest.car.zst` format that can be used for
+//! 1. Export an effective standard lite snapshot in `.forest.car.zst` format that can be used for
 //!    bootstrapping a Filecoin node into the CAR database.
 //! 2. Stop the node.
-//! 3. Purging parity-db columns that serve as non-persistent blockstore.
-//! 4. Purging old CAR database files.
+//! 3. Purge parity-db columns that serve as non-persistent blockstore.
+//! 4. Purge old CAR database files.
 //! 5. Restart the node.
 //!
 //! ## Correctness
@@ -30,7 +30,8 @@
 //! roughly 2.5 GiB.
 //!
 //! ## Scheduling
-//! To be implemented
+//! When automatic GC is enabled, it by default runs every 7 days (20160 epochs).
+//! The interval can be overridden by setting environment variable `FOREST_SNAPSHOT_GC_INTERVAL_EPOCHS`.
 //!
 //! ## Performance
 //! The lite snapshot export step is currently utilizing a depth-first search algorithm, with `O(V+E)` complexity,
@@ -45,13 +46,12 @@
 use crate::blocks::Tipset;
 use crate::cid_collections::CidHashSet;
 use crate::cli_shared::chain_path;
-use crate::db::SettingsStoreExt;
 use crate::db::{
-    CAR_DB_DIR_NAME, SettingsStore,
-    car::forest::FOREST_CAR_FILE_EXTENSION,
+    CAR_DB_DIR_NAME, HeaviestTipsetKeyProvider, SettingsStore, SettingsStoreExt,
     db_engine::{DbConfig, db_root},
     parity_db::{DbColumn, ParityDb},
 };
+use crate::shim::clock::{ChainEpoch, EPOCHS_IN_DAY};
 use anyhow::Context as _;
 use fvm_ipld_blockstore::Blockstore;
 use parking_lot::RwLock;
@@ -72,6 +72,7 @@ pub struct SnapshotGarbageCollector<DB> {
     exported_chain_head: RwLock<Option<Tipset>>,
     blessed_lite_snapshot: RwLock<Option<PathBuf>>,
     db: RwLock<Option<Arc<DB>>>,
+    car_db_head_epoch: RwLock<Option<ChainEpoch>>,
     reboot_tx: flume::Sender<()>,
     trigger_tx: flume::Sender<()>,
     trigger_rx: flume::Receiver<()>,
@@ -80,7 +81,7 @@ pub struct SnapshotGarbageCollector<DB> {
 
 impl<DB> SnapshotGarbageCollector<DB>
 where
-    DB: Blockstore + SettingsStore + Send + Sync + 'static,
+    DB: Blockstore + SettingsStore + HeaviestTipsetKeyProvider + Send + Sync + 'static,
 {
     pub fn new(config: &crate::Config) -> anyhow::Result<(Self, flume::Receiver<()>)> {
         let chain_data_path = chain_path(config);
@@ -99,6 +100,7 @@ where
                 exported_chain_head: RwLock::new(None),
                 blessed_lite_snapshot: RwLock::new(None),
                 db: RwLock::new(None),
+                car_db_head_epoch: RwLock::new(None),
                 reboot_tx,
                 trigger_tx,
                 trigger_rx,
@@ -112,7 +114,11 @@ where
         *self.db.write() = Some(db);
     }
 
-    pub async fn event_toop(&self) {
+    pub fn set_car_db_head_epoch(&self, epoch: ChainEpoch) {
+        *self.car_db_head_epoch.write() = Some(epoch);
+    }
+
+    pub async fn event_loop(&self) {
         while self.trigger_rx.recv_async().await.is_ok() {
             if self.running.load(Ordering::Relaxed) {
                 tracing::warn!("snap gc has already been running");
@@ -125,6 +131,52 @@ where
         }
     }
 
+    pub async fn scheduler_loop(&self) {
+        let snap_gc_interval_epochs = std::env::var("FOREST_SNAPSHOT_GC_INTERVAL_EPOCHS")
+            .ok()
+            .and_then(|i| i.parse().ok())
+            .inspect(|i| {
+                tracing::info!(
+                    "Using snapshot GC interval epochs {i} set by FOREST_F3_BOOTSTRAP_EPOCH"
+                )
+            })
+            .unwrap_or(EPOCHS_IN_DAY * 7);
+        let snap_gc_check_interval_secs = std::env::var("FOREST_SNAPSHOT_GC_CHECK_INTERVAL_SECONDS")
+            .ok()
+            .and_then(|i| i.parse().ok())
+            .inspect(|i| {
+                tracing::info!(
+                    "Using snapshot GC check interval seconds {i} set by FOREST_SNAPSHOT_GC_CHECK_INTERVAL_SECONDS"
+                )
+            })
+            .unwrap_or(60 * 5);
+        let snap_gc_check_interval = Duration::from_secs(snap_gc_check_interval_secs);
+        tracing::info!(
+            "Running snapshot GC scheduler with interval epochs {snap_gc_interval_epochs}"
+        );
+        loop {
+            if !self.running.load(Ordering::Relaxed) {
+                if let (Some(db), Some(car_db_head_epoch)) =
+                    (&*self.db.read(), *self.car_db_head_epoch.read())
+                {
+                    if let Ok(head_key) = HeaviestTipsetKeyProvider::heaviest_tipset_key(db) {
+                        if let Ok(head) = Tipset::load_required(db, &head_key) {
+                            let head_epoch = head.epoch();
+                            if head_epoch - car_db_head_epoch >= snap_gc_interval_epochs
+                                && self.trigger_tx.try_send(()).is_ok()
+                            {
+                                tracing::info!(%car_db_head_epoch, %head_epoch, %snap_gc_interval_epochs, "Snap GC scheduled");
+                            } else {
+                                tracing::trace!(%car_db_head_epoch, %head_epoch, %snap_gc_interval_epochs, "Snap GC not scheduled");
+                            }
+                        }
+                    }
+                }
+            }
+            tokio::time::sleep(snap_gc_check_interval).await;
+        }
+    }
+
     pub fn trigger(&self) -> anyhow::Result<flume::Receiver<()>> {
         if self.running.load(Ordering::Relaxed) {
             anyhow::bail!("snap gc has already been running");
@@ -182,6 +234,7 @@ where
     async fn cleanup_before_reboot_inner(&self) -> anyhow::Result<()> {
         tracing::info!("cleaning up db before rebooting");
         {
+            // Release db references so that it can be closed
             if let (Some(db), Some(head)) =
                 (self.db.write().take(), &*self.exported_chain_head.read())
             {
@@ -216,12 +269,11 @@ where
                     .into_iter()
                     .filter_map(|entry| {
                         if let Ok(entry) = entry {
-                            if entry.path() != blessed_lite_snapshot.as_path() {
-                                if let Some(filename) = entry.file_name().to_str() {
-                                    if filename.ends_with(FOREST_CAR_FILE_EXTENSION) {
-                                        return Some(entry.into_path());
-                                    }
-                                }
+                            // Also cleanup `.tmp*` files snapshot export is interrupted ungracefully
+                            if entry.path().is_file()
+                                && entry.path() != blessed_lite_snapshot.as_path()
+                            {
+                                return Some(entry.into_path());
                             }
                         }
                         None


Reviews:
user: login: LesnyRumcajs
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T21:39:23Z
user: login: hanabi1224
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T22:37:30Z
user: login: hanabi1224
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T22:38:10Z
user: login: hanabi1224
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T10:23:21Z
user: login: sudo-shashank
body: @hanabi1224 we should also update the gc guide to include info about new env vars and default values 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T05:20:33Z
user: login: sudo-shashank
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T02:10:27Z
user: login: LesnyRumcajs
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-22T16:19:52Z

Review comments:
diff_hunk: @@ -48,6 +48,7 @@ process.
 | `FOREST_CHAIN_INDEXER_ENABLED`                            | 1 or true                        | false                                          | 1                                                             | Whether or not to index the chain to support the Ethereum RPC API                                                     |
 | `FOREST_MESSAGES_IN_TIPSET_CACHE_SIZE`                    | positive integer                 | 100                                            | 42                                                            | The size of an internal cache of tipsets to messages                                                                  |
 | `FOREST_STATE_MIGRATION_DB_WRITE_BUFFER`                  | non-negative integer             | 10000                                          | 100000                                                        | The size of db write buffer for state migration (`~10MB` RAM per `10k` buffer)                                        |
+| `FOREST_SNAPSHOT_GC_INTERVAL_EPOCHS`                      | non-negative integer             | 20160                                          | 8000                                                          | The interval in epochs for scheduling snapshot GC                                                                     |
path: docs/docs/users/reference/env_variables.md
user: login: LesnyRumcajs
body: Shouldn't it be a positive integer? Or in other words, what would `0` in the interval mean? On every CPU cycle?
created_at: 2025-05-15T20:54:37Z
author_association: MEMBER
diff_hunk: @@ -125,6 +130,42 @@ where
         }
     }
 
+    pub async fn scheduler_loop(&self) {
+        let snap_gc_interval_epochs = std::env::var("FOREST_SNAPSHOT_GC_INTERVAL_EPOCHS")
+            .ok()
+            .and_then(|i| i.parse().ok())
+            .inspect(|i| {
+                tracing::info!(
+                    "Using snapshot GC interval epochs {i} set by FOREST_F3_BOOTSTRAP_EPOCH"
+                )
+            })
+            .unwrap_or(EPOCHS_IN_DAY * 7);
+        tracing::info!(
+            "Running snapshot GC scheduler with interval epochs {snap_gc_interval_epochs}"
+        );
+        loop {
+            if !self.running.load(Ordering::Relaxed) {
+                if let (Some(db), Some(car_db_head_epoch)) =
+                    (&*self.db.read(), *self.car_db_head_epoch.read())
+                {
+                    if let Ok(head_key) = HeaviestTipsetKeyProvider::heaviest_tipset_key(db) {
+                        if let Ok(head) = Tipset::load_required(db, &head_key) {
+                            let head_epoch = head.epoch();
+                            if head_epoch - car_db_head_epoch >= snap_gc_interval_epochs
+                                && self.trigger_tx.try_send(()).is_ok()
+                            {
+                                tracing::info!(%car_db_head_epoch, %head_epoch, %snap_gc_interval_epochs, "Snap GC scheduled");
+                            } else {
+                                tracing::trace!(%car_db_head_epoch, %head_epoch, %snap_gc_interval_epochs, "Snap GC not scheduled");
+                            }
+                        }
+                    }
+                }
+            }
+            tokio::time::sleep(Duration::from_secs(60 * 5)).await;
path: src/db/gc/snapshot.rs
user: login: LesnyRumcajs
body: I'd make it configurable.
created_at: 2025-05-15T21:38:42Z
author_association: MEMBER
diff_hunk: @@ -125,6 +130,42 @@ where
         }
     }
 
+    pub async fn scheduler_loop(&self) {
+        let snap_gc_interval_epochs = std::env::var("FOREST_SNAPSHOT_GC_INTERVAL_EPOCHS")
+            .ok()
+            .and_then(|i| i.parse().ok())
+            .inspect(|i| {
+                tracing::info!(
+                    "Using snapshot GC interval epochs {i} set by FOREST_F3_BOOTSTRAP_EPOCH"
+                )
+            })
+            .unwrap_or(EPOCHS_IN_DAY * 7);
path: src/db/gc/snapshot.rs
user: login: LesnyRumcajs
body: Is this documented anywhere?
created_at: 2025-05-15T21:38:59Z
author_association: MEMBER
diff_hunk: @@ -48,6 +48,7 @@ process.
 | `FOREST_CHAIN_INDEXER_ENABLED`                            | 1 or true                        | false                                          | 1                                                             | Whether or not to index the chain to support the Ethereum RPC API                                                     |
 | `FOREST_MESSAGES_IN_TIPSET_CACHE_SIZE`                    | positive integer                 | 100                                            | 42                                                            | The size of an internal cache of tipsets to messages                                                                  |
 | `FOREST_STATE_MIGRATION_DB_WRITE_BUFFER`                  | non-negative integer             | 10000                                          | 100000                                                        | The size of db write buffer for state migration (`~10MB` RAM per `10k` buffer)                                        |
+| `FOREST_SNAPSHOT_GC_INTERVAL_EPOCHS`                      | non-negative integer             | 20160                                          | 8000                                                          | The interval in epochs for scheduling snapshot GC                                                                     |
path: docs/docs/users/reference/env_variables.md
user: login: hanabi1224
body: 0 means running GC continuously, which does not make a significant difference from 1
created_at: 2025-05-15T22:37:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -125,6 +130,42 @@ where
         }
     }
 
+    pub async fn scheduler_loop(&self) {
+        let snap_gc_interval_epochs = std::env::var("FOREST_SNAPSHOT_GC_INTERVAL_EPOCHS")
+            .ok()
+            .and_then(|i| i.parse().ok())
+            .inspect(|i| {
+                tracing::info!(
+                    "Using snapshot GC interval epochs {i} set by FOREST_F3_BOOTSTRAP_EPOCH"
+                )
+            })
+            .unwrap_or(EPOCHS_IN_DAY * 7);
path: src/db/gc/snapshot.rs
user: login: hanabi1224
body: The default value of the env var
created_at: 2025-05-15T22:38:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -125,6 +130,42 @@ where
         }
     }
 
+    pub async fn scheduler_loop(&self) {
+        let snap_gc_interval_epochs = std::env::var("FOREST_SNAPSHOT_GC_INTERVAL_EPOCHS")
+            .ok()
+            .and_then(|i| i.parse().ok())
+            .inspect(|i| {
+                tracing::info!(
+                    "Using snapshot GC interval epochs {i} set by FOREST_F3_BOOTSTRAP_EPOCH"
+                )
+            })
+            .unwrap_or(EPOCHS_IN_DAY * 7);
+        tracing::info!(
+            "Running snapshot GC scheduler with interval epochs {snap_gc_interval_epochs}"
+        );
+        loop {
+            if !self.running.load(Ordering::Relaxed) {
+                if let (Some(db), Some(car_db_head_epoch)) =
+                    (&*self.db.read(), *self.car_db_head_epoch.read())
+                {
+                    if let Ok(head_key) = HeaviestTipsetKeyProvider::heaviest_tipset_key(db) {
+                        if let Ok(head) = Tipset::load_required(db, &head_key) {
+                            let head_epoch = head.epoch();
+                            if head_epoch - car_db_head_epoch >= snap_gc_interval_epochs
+                                && self.trigger_tx.try_send(()).is_ok()
+                            {
+                                tracing::info!(%car_db_head_epoch, %head_epoch, %snap_gc_interval_epochs, "Snap GC scheduled");
+                            } else {
+                                tracing::trace!(%car_db_head_epoch, %head_epoch, %snap_gc_interval_epochs, "Snap GC not scheduled");
+                            }
+                        }
+                    }
+                }
+            }
+            tokio::time::sleep(Duration::from_secs(60 * 5)).await;
path: src/db/gc/snapshot.rs
user: login: hanabi1224
body: Done.
created_at: 2025-05-16T10:23:21Z
author_association: CONTRIBUTOR

PR comments:
user: login: LesnyRumcajs
created_at: 2025-05-16T14:57:10Z
body: @hanabi1224 What about this piece of documentation? https://docs.forest.chainsafe.io/guides/gc
author_association: MEMBER
user: login: LesnyRumcajs
created_at: 2025-05-21T07:21:12Z
body: @hanabi1224?
author_association: MEMBER
user: login: hanabi1224
created_at: 2025-05-21T11:52:43Z
body: @LesnyRumcajs @sudo-shashank doc updated
author_association: CONTRIBUTOR