Skip to main content

meta_srv/gc/
options.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::time::Duration;
16
17use serde::{Deserialize, Serialize};
18use snafu::ensure;
19
20use crate::error::{self, Result};
21
22/// The interval of the gc ticker.
23#[allow(unused)]
24pub(crate) const TICKER_INTERVAL: Duration = Duration::from_secs(60 * 5);
25
26/// Configuration for garbage collecting soft-dropped tables.
27#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
28#[serde(default)]
29pub struct SoftDropGcOptions {
30    /// Whether soft drop is enabled.
31    pub enable: bool,
32    /// How long soft-dropped tables are retained before automatic purge.
33    #[serde(with = "humantime_serde")]
34    pub retention: Duration,
35}
36
37impl Default for SoftDropGcOptions {
38    fn default() -> Self {
39        Self {
40            enable: false,
41            retention: Duration::from_days(7),
42        }
43    }
44}
45
46/// Configuration for GC operations.
47///
48/// TODO(discord9): not expose most config to users for now, until GC scheduler is fully stable.
49#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
50#[serde(default)]
51pub struct GcSchedulerOptions {
52    /// Whether GC is enabled. Default to false.
53    /// If set to false, no GC will be performed, and potentially some
54    /// files from datanodes will never be deleted.
55    pub enable: bool,
56    /// Experimental soft-drop garbage collection options.
57    pub experimental_soft_drop: SoftDropGcOptions,
58    /// Maximum number of tables to process concurrently.
59    pub max_concurrent_tables: usize,
60    /// Maximum number of retries per region when GC fails.
61    pub max_retries_per_region: usize,
62    /// Concurrency for region GC within a table.
63    pub region_gc_concurrency: usize,
64    /// Backoff duration between retries.
65    #[serde(with = "humantime_serde")]
66    pub retry_backoff_duration: Duration,
67    /// Minimum region size threshold for GC (in bytes).
68    pub min_region_size_threshold: u64,
69    /// Weight for SST file count in GC scoring.
70    pub sst_count_weight: f64,
71    /// Weight for file removal rate in GC scoring.
72    pub file_removed_count_weight: f64,
73    /// Cooldown period between GC operations on the same region.
74    #[serde(with = "humantime_serde")]
75    pub gc_cooldown_period: Duration,
76    /// Maximum number of regions to select for GC per table.
77    pub regions_per_table_threshold: usize,
78    /// Timeout duration for mailbox communication with datanodes.
79    #[serde(with = "humantime_serde")]
80    pub mailbox_timeout: Duration,
81    /// Interval for performing full file listing during GC to find orphan files.
82    /// Full file listing is expensive but necessary to clean up orphan files.
83    /// Set to a larger value (e.g., 24 hours) to balance performance and cleanup.
84    /// Every Nth GC cycle will use full file listing, where N = full_file_listing_interval / TICKER_INTERVAL.
85    #[serde(with = "humantime_serde")]
86    pub full_file_listing_interval: Duration,
87    /// Interval for cleaning up stale region entries from the GC tracker.
88    /// This removes entries for regions that no longer exist (e.g., after table drops).
89    /// Set to a larger value (e.g., 6 hours) since this is just for memory cleanup.
90    #[serde(with = "humantime_serde")]
91    pub tracker_cleanup_interval: Duration,
92}
93
94impl Default for GcSchedulerOptions {
95    fn default() -> Self {
96        Self {
97            enable: false,
98            experimental_soft_drop: SoftDropGcOptions::default(),
99            max_concurrent_tables: 10,
100            max_retries_per_region: 3,
101            retry_backoff_duration: Duration::from_secs(5),
102            region_gc_concurrency: 16,
103            min_region_size_threshold: 100 * 1024 * 1024, // 100MB
104            sst_count_weight: 0.5, // more sst means could potentially remove more files, moderate priority
105            file_removed_count_weight: 1.0, // more file to be deleted, higher priority
106            gc_cooldown_period: Duration::from_secs(60 * 5), // 5 minutes
107            regions_per_table_threshold: 20, // Select top 20 regions per table
108            mailbox_timeout: Duration::from_secs(60), // 60 seconds
109            // Perform full file listing every 24 hours to find orphan files
110            full_file_listing_interval: Duration::from_secs(60 * 60 * 24),
111            // Clean up stale tracker entries every 6 hours
112            tracker_cleanup_interval: Duration::from_secs(60 * 60 * 6),
113        }
114    }
115}
116
117impl GcSchedulerOptions {
118    /// Validates the configuration options.
119    pub fn validate(&self) -> Result<()> {
120        #[cfg(not(feature = "enterprise"))]
121        ensure!(
122            !self.experimental_soft_drop.enable,
123            error::InvalidArgumentsSnafu {
124                err_msg: "gc.experimental_soft_drop.enable is only available in GreptimeDB Enterprise Edition",
125            }
126        );
127
128        ensure!(
129            !self.experimental_soft_drop.enable || self.enable,
130            error::InvalidArgumentsSnafu {
131                err_msg: "gc.enable must be true when soft drop is enabled",
132            }
133        );
134
135        if !self.enable {
136            return Ok(());
137        }
138
139        if self.experimental_soft_drop.enable {
140            ensure!(
141                self.experimental_soft_drop.retention.as_millis() > 0,
142                error::InvalidArgumentsSnafu {
143                    err_msg: "soft drop retention must be at least 1ms",
144                }
145            );
146            ensure!(
147                self.experimental_soft_drop.retention.as_millis() <= i64::MAX as u128,
148                error::InvalidArgumentsSnafu {
149                    err_msg: "soft drop retention must fit in an i64 millisecond value",
150                }
151            );
152        }
153
154        ensure!(
155            self.max_concurrent_tables > 0,
156            error::InvalidArgumentsSnafu {
157                err_msg: "max_concurrent_tables must be greater than 0",
158            }
159        );
160
161        ensure!(
162            self.max_retries_per_region > 0,
163            error::InvalidArgumentsSnafu {
164                err_msg: "max_retries_per_region must be greater than 0",
165            }
166        );
167
168        ensure!(
169            self.region_gc_concurrency > 0,
170            error::InvalidArgumentsSnafu {
171                err_msg: "region_gc_concurrency must be greater than 0",
172            }
173        );
174
175        ensure!(
176            !self.retry_backoff_duration.is_zero(),
177            error::InvalidArgumentsSnafu {
178                err_msg: "retry_backoff_duration must be greater than 0",
179            }
180        );
181
182        ensure!(
183            self.sst_count_weight >= 0.0,
184            error::InvalidArgumentsSnafu {
185                err_msg: "sst_count_weight must be non-negative",
186            }
187        );
188
189        ensure!(
190            self.file_removed_count_weight >= 0.0,
191            error::InvalidArgumentsSnafu {
192                err_msg: "file_removal_rate_weight must be non-negative",
193            }
194        );
195
196        ensure!(
197            !self.gc_cooldown_period.is_zero(),
198            error::InvalidArgumentsSnafu {
199                err_msg: "gc_cooldown_period must be greater than 0",
200            }
201        );
202
203        ensure!(
204            self.regions_per_table_threshold > 0,
205            error::InvalidArgumentsSnafu {
206                err_msg: "regions_per_table_threshold must be greater than 0",
207            }
208        );
209
210        ensure!(
211            !self.mailbox_timeout.is_zero(),
212            error::InvalidArgumentsSnafu {
213                err_msg: "mailbox_timeout must be greater than 0",
214            }
215        );
216
217        ensure!(
218            !self.full_file_listing_interval.is_zero(),
219            error::InvalidArgumentsSnafu {
220                err_msg: "full_file_listing_interval must be greater than 0",
221            }
222        );
223
224        ensure!(
225            !self.tracker_cleanup_interval.is_zero(),
226            error::InvalidArgumentsSnafu {
227                err_msg: "tracker_cleanup_interval must be greater than 0",
228            }
229        );
230
231        Ok(())
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238
239    #[test]
240    fn test_soft_drop_defaults() {
241        let options = GcSchedulerOptions::default();
242
243        assert!(!options.experimental_soft_drop.enable);
244        assert_eq!(
245            Duration::from_days(7),
246            options.experimental_soft_drop.retention
247        );
248    }
249
250    #[cfg(feature = "enterprise")]
251    #[test]
252    fn test_soft_drop_valid_when_gc_is_enabled() {
253        let options = GcSchedulerOptions {
254            enable: true,
255            experimental_soft_drop: SoftDropGcOptions {
256                enable: true,
257                retention: Duration::from_days(1),
258            },
259            ..Default::default()
260        };
261
262        assert!(options.validate().is_ok());
263    }
264
265    #[cfg(not(feature = "enterprise"))]
266    #[test]
267    fn test_soft_drop_rejected_in_non_enterprise_build() {
268        let options = GcSchedulerOptions {
269            enable: true,
270            experimental_soft_drop: SoftDropGcOptions {
271                enable: true,
272                retention: Duration::from_days(1),
273            },
274            ..Default::default()
275        };
276
277        let err = options.validate().unwrap_err();
278        assert!(err.to_string().contains("Enterprise Edition"));
279    }
280
281    #[cfg(feature = "enterprise")]
282    #[test]
283    fn test_soft_drop_requires_gc() {
284        let options = GcSchedulerOptions {
285            experimental_soft_drop: SoftDropGcOptions {
286                enable: true,
287                retention: Duration::from_days(1),
288            },
289            ..Default::default()
290        };
291
292        assert!(options.validate().is_err());
293    }
294
295    #[cfg(feature = "enterprise")]
296    #[test]
297    fn test_soft_drop_retention_must_be_positive() {
298        let options = GcSchedulerOptions {
299            enable: true,
300            experimental_soft_drop: SoftDropGcOptions {
301                enable: true,
302                retention: Duration::ZERO,
303            },
304            ..Default::default()
305        };
306
307        assert!(options.validate().is_err());
308    }
309
310    #[cfg(feature = "enterprise")]
311    #[test]
312    fn test_soft_drop_retention_must_be_at_least_one_millisecond() {
313        let options = GcSchedulerOptions {
314            enable: true,
315            experimental_soft_drop: SoftDropGcOptions {
316                enable: true,
317                retention: Duration::from_micros(999),
318            },
319            ..Default::default()
320        };
321
322        assert!(options.validate().is_err());
323    }
324
325    #[cfg(feature = "enterprise")]
326    #[test]
327    fn test_soft_drop_retention_must_fit_i64_millis() {
328        let options = GcSchedulerOptions {
329            enable: true,
330            experimental_soft_drop: SoftDropGcOptions {
331                enable: true,
332                retention: Duration::from_millis(i64::MAX as u64 + 1),
333            },
334            ..Default::default()
335        };
336
337        assert!(options.validate().is_err());
338    }
339}