1 //! Module for configuring the cache system.
2
3 use directories_next::ProjectDirs;
4 use log::{trace, warn};
5 use serde::{
6 Deserialize,
7 de::{self, Deserializer},
8 };
9 use std::fmt::Debug;
10 use std::fs;
11 use std::path::{Path, PathBuf};
12 use std::time::Duration;
13 use wasmtime_environ::prelude::*;
14
15 // wrapped, so we have named section in config,
16 // also, for possible future compatibility
17 #[derive(serde_derive::Deserialize, Debug)]
18 #[serde(deny_unknown_fields)]
19 struct Config {
20 cache: CacheConfig,
21 }
22
23 /// Global configuration for how the cache is managed
24 #[derive(serde_derive::Deserialize, Debug, Clone)]
25 #[serde(deny_unknown_fields)]
26 pub struct CacheConfig {
27 directory: Option<PathBuf>,
28 #[serde(
29 default = "default_worker_event_queue_size",
30 rename = "worker-event-queue-size",
31 deserialize_with = "deserialize_si_prefix"
32 )]
33 worker_event_queue_size: u64,
34 #[serde(
35 default = "default_baseline_compression_level",
36 rename = "baseline-compression-level"
37 )]
38 baseline_compression_level: i32,
39 #[serde(
40 default = "default_optimized_compression_level",
41 rename = "optimized-compression-level"
42 )]
43 optimized_compression_level: i32,
44 #[serde(
45 default = "default_optimized_compression_usage_counter_threshold",
46 rename = "optimized-compression-usage-counter-threshold",
47 deserialize_with = "deserialize_si_prefix"
48 )]
49 optimized_compression_usage_counter_threshold: u64,
50 #[serde(
51 default = "default_cleanup_interval",
52 rename = "cleanup-interval",
53 deserialize_with = "deserialize_duration"
54 )]
55 cleanup_interval: Duration,
56 #[serde(
57 default = "default_optimizing_compression_task_timeout",
58 rename = "optimizing-compression-task-timeout",
59 deserialize_with = "deserialize_duration"
60 )]
61 optimizing_compression_task_timeout: Duration,
62 #[serde(
63 default = "default_allowed_clock_drift_for_files_from_future",
64 rename = "allowed-clock-drift-for-files-from-future",
65 deserialize_with = "deserialize_duration"
66 )]
67 allowed_clock_drift_for_files_from_future: Duration,
68 #[serde(
69 default = "default_file_count_soft_limit",
70 rename = "file-count-soft-limit",
71 deserialize_with = "deserialize_si_prefix"
72 )]
73 file_count_soft_limit: u64,
74 #[serde(
75 default = "default_files_total_size_soft_limit",
76 rename = "files-total-size-soft-limit",
77 deserialize_with = "deserialize_disk_space"
78 )]
79 files_total_size_soft_limit: u64,
80 #[serde(
81 default = "default_file_count_limit_percent_if_deleting",
82 rename = "file-count-limit-percent-if-deleting",
83 deserialize_with = "deserialize_percent"
84 )]
85 file_count_limit_percent_if_deleting: u8,
86 #[serde(
87 default = "default_files_total_size_limit_percent_if_deleting",
88 rename = "files-total-size-limit-percent-if-deleting",
89 deserialize_with = "deserialize_percent"
90 )]
91 files_total_size_limit_percent_if_deleting: u8,
92 }
93
94 impl Default for CacheConfig {
default() -> Self95 fn default() -> Self {
96 Self {
97 directory: None,
98 worker_event_queue_size: default_worker_event_queue_size(),
99 baseline_compression_level: default_baseline_compression_level(),
100 optimized_compression_level: default_optimized_compression_level(),
101 optimized_compression_usage_counter_threshold:
102 default_optimized_compression_usage_counter_threshold(),
103 cleanup_interval: default_cleanup_interval(),
104 optimizing_compression_task_timeout: default_optimizing_compression_task_timeout(),
105 allowed_clock_drift_for_files_from_future:
106 default_allowed_clock_drift_for_files_from_future(),
107 file_count_soft_limit: default_file_count_soft_limit(),
108 files_total_size_soft_limit: default_files_total_size_soft_limit(),
109 file_count_limit_percent_if_deleting: default_file_count_limit_percent_if_deleting(),
110 files_total_size_limit_percent_if_deleting:
111 default_files_total_size_limit_percent_if_deleting(),
112 }
113 }
114 }
115
116 /// Creates a new configuration file at specified path, or default path if None is passed.
117 /// Fails if file already exists.
create_new_config<P: AsRef<Path> + Debug>(config_file: Option<P>) -> Result<PathBuf>118 pub fn create_new_config<P: AsRef<Path> + Debug>(config_file: Option<P>) -> Result<PathBuf> {
119 trace!("Creating new config file, path: {config_file:?}");
120
121 let config_file = match config_file {
122 Some(path) => path.as_ref().to_path_buf(),
123 None => default_config_path()?,
124 };
125
126 if config_file.exists() {
127 bail!(
128 "Configuration file '{}' already exists.",
129 config_file.display()
130 );
131 }
132
133 let parent_dir = config_file
134 .parent()
135 .ok_or_else(|| format_err!("Invalid cache config path: {}", config_file.display()))?;
136
137 fs::create_dir_all(parent_dir).with_context(|| {
138 format!(
139 "Failed to create config directory, config path: {}",
140 config_file.display(),
141 )
142 })?;
143
144 let content = "\
145 # Comment out certain settings to use default values.
146 # For more settings, please refer to the documentation:
147 # https://bytecodealliance.github.io/wasmtime/cli-cache.html
148
149 [cache]
150 ";
151
152 fs::write(&config_file, content).with_context(|| {
153 format!(
154 "Failed to flush config to the disk, path: {}",
155 config_file.display(),
156 )
157 })?;
158
159 Ok(config_file.to_path_buf())
160 }
161
162 // permitted levels from: https://docs.rs/zstd/0.4.28+zstd.1.4.3/zstd/stream/write/struct.Encoder.html
163 const ZSTD_COMPRESSION_LEVELS: std::ops::RangeInclusive<i32> = 0..=21;
164
165 // Default settings, you're welcome to tune them!
166 // TODO: what do we want to warn users about?
167
168 // At the moment of writing, the modules couldn't depend on another,
169 // so we have at most one module per wasmtime instance
170 // if changed, update cli-cache.md
default_worker_event_queue_size() -> u64171 const fn default_worker_event_queue_size() -> u64 {
172 0x10
173 }
worker_event_queue_size_warning_threshold() -> u64174 const fn worker_event_queue_size_warning_threshold() -> u64 {
175 3
176 }
177 // should be quick and provide good enough compression
178 // if changed, update cli-cache.md
default_baseline_compression_level() -> i32179 const fn default_baseline_compression_level() -> i32 {
180 zstd::DEFAULT_COMPRESSION_LEVEL
181 }
182 // should provide significantly better compression than baseline
183 // if changed, update cli-cache.md
default_optimized_compression_level() -> i32184 const fn default_optimized_compression_level() -> i32 {
185 20
186 }
187 // shouldn't be to low to avoid recompressing too many files
188 // if changed, update cli-cache.md
default_optimized_compression_usage_counter_threshold() -> u64189 const fn default_optimized_compression_usage_counter_threshold() -> u64 {
190 0x100
191 }
192 // if changed, update cli-cache.md
default_cleanup_interval() -> Duration193 const fn default_cleanup_interval() -> Duration {
194 Duration::from_secs(60 * 60)
195 }
196 // if changed, update cli-cache.md
default_optimizing_compression_task_timeout() -> Duration197 const fn default_optimizing_compression_task_timeout() -> Duration {
198 Duration::from_secs(30 * 60)
199 }
200 // the default assumes problems with timezone configuration on network share + some clock drift
201 // please notice 24 timezones = max 23h difference between some of them
202 // if changed, update cli-cache.md
default_allowed_clock_drift_for_files_from_future() -> Duration203 const fn default_allowed_clock_drift_for_files_from_future() -> Duration {
204 Duration::from_secs(60 * 60 * 24)
205 }
206 // if changed, update cli-cache.md
default_file_count_soft_limit() -> u64207 const fn default_file_count_soft_limit() -> u64 {
208 0x10_000
209 }
210 // if changed, update cli-cache.md
default_files_total_size_soft_limit() -> u64211 const fn default_files_total_size_soft_limit() -> u64 {
212 1024 * 1024 * 512
213 }
214 // if changed, update cli-cache.md
default_file_count_limit_percent_if_deleting() -> u8215 const fn default_file_count_limit_percent_if_deleting() -> u8 {
216 70
217 }
218 // if changed, update cli-cache.md
default_files_total_size_limit_percent_if_deleting() -> u8219 const fn default_files_total_size_limit_percent_if_deleting() -> u8 {
220 70
221 }
222
project_dirs() -> Option<ProjectDirs>223 fn project_dirs() -> Option<ProjectDirs> {
224 ProjectDirs::from("", "BytecodeAlliance", "wasmtime")
225 }
226
default_config_path() -> Result<PathBuf>227 fn default_config_path() -> Result<PathBuf> {
228 match project_dirs() {
229 Some(dirs) => Ok(dirs.config_dir().join("config.toml")),
230 None => bail!("config file not specified and failed to get the default"),
231 }
232 }
233
234 // Deserializers of our custom formats
235 // can be replaced with const generics later
236 macro_rules! generate_deserializer {
237 ($name:ident($numname:ident: $numty:ty, $unitname:ident: &str) -> $retty:ty {$body:expr}) => {
238 fn $name<'de, D>(deserializer: D) -> Result<$retty, D::Error>
239 where
240 D: Deserializer<'de>,
241 {
242 let text = String::deserialize(deserializer)?;
243 let text = text.trim();
244 let split_point = text.find(|c: char| !c.is_numeric());
245 let (num, unit) = split_point.map_or_else(|| (text, ""), |p| text.split_at(p));
246 let deserialized = (|| {
247 let $numname = num.parse::<$numty>().ok()?;
248 let $unitname = unit.trim();
249 $body
250 })();
251 if let Some(deserialized) = deserialized {
252 Ok(deserialized)
253 } else {
254 Err(de::Error::custom(
255 "Invalid value, please refer to the documentation",
256 ))
257 }
258 }
259 };
260 }
261
262 generate_deserializer!(deserialize_duration(num: u64, unit: &str) -> Duration {
263 match unit {
264 "s" => Some(Duration::from_secs(num)),
265 "m" => Some(Duration::from_secs(num * 60)),
266 "h" => Some(Duration::from_secs(num * 60 * 60)),
267 "d" => Some(Duration::from_secs(num * 60 * 60 * 24)),
268 _ => None,
269 }
270 });
271
272 generate_deserializer!(deserialize_si_prefix(num: u64, unit: &str) -> u64 {
273 match unit {
274 "" => Some(num),
275 "K" => num.checked_mul(1_000),
276 "M" => num.checked_mul(1_000_000),
277 "G" => num.checked_mul(1_000_000_000),
278 "T" => num.checked_mul(1_000_000_000_000),
279 "P" => num.checked_mul(1_000_000_000_000_000),
280 _ => None,
281 }
282 });
283
284 generate_deserializer!(deserialize_disk_space(num: u64, unit: &str) -> u64 {
285 match unit {
286 "" => Some(num),
287 "K" => num.checked_mul(1_000),
288 "Ki" => num.checked_mul(1u64 << 10),
289 "M" => num.checked_mul(1_000_000),
290 "Mi" => num.checked_mul(1u64 << 20),
291 "G" => num.checked_mul(1_000_000_000),
292 "Gi" => num.checked_mul(1u64 << 30),
293 "T" => num.checked_mul(1_000_000_000_000),
294 "Ti" => num.checked_mul(1u64 << 40),
295 "P" => num.checked_mul(1_000_000_000_000_000),
296 "Pi" => num.checked_mul(1u64 << 50),
297 _ => None,
298 }
299 });
300
301 generate_deserializer!(deserialize_percent(num: u8, unit: &str) -> u8 {
302 match unit {
303 "%" => Some(num),
304 _ => None,
305 }
306 });
307
308 macro_rules! generate_setting_getter {
309 ($setting:ident: $setting_type:ty) => {
310 #[doc = concat!("Returns ", "`", stringify!($setting), "`.")]
311 pub fn $setting(&self) -> $setting_type {
312 self.$setting
313 }
314 };
315 }
316
317 impl CacheConfig {
318 /// Creates a cache configuration with default settings.
new() -> Self319 pub fn new() -> Self {
320 Self::default()
321 }
322
323 /// Loads cache configuration specified at `path`.
324 ///
325 /// This method will read the file specified by `path` on the filesystem and
326 /// attempt to load cache configuration from it. This method can also fail
327 /// due to I/O errors, misconfiguration, syntax errors, etc. For expected
328 /// syntax in the configuration file see the [documentation online][docs].
329 ///
330 /// Passing in `None` loads cache configuration from the system default path.
331 /// This is located, for example, on Unix at `$HOME/.config/wasmtime/config.toml`
332 /// and is typically created with the `wasmtime config new` command.
333 ///
334 /// # Errors
335 ///
336 /// This method can fail due to any error that happens when loading the file
337 /// pointed to by `path` and attempting to load the cache configuration.
338 ///
339 /// [docs]: https://bytecodealliance.github.io/wasmtime/cli-cache.html
from_file(config_file: Option<&Path>) -> Result<Self>340 pub fn from_file(config_file: Option<&Path>) -> Result<Self> {
341 let mut config = Self::load_and_parse_file(config_file)?;
342 config.validate()?;
343 Ok(config)
344 }
345
load_and_parse_file(config_file: Option<&Path>) -> Result<Self>346 fn load_and_parse_file(config_file: Option<&Path>) -> Result<Self> {
347 // get config file path
348 let (config_file, user_custom_file) = match config_file {
349 Some(path) => (path.to_path_buf(), true),
350 None => (default_config_path()?, false),
351 };
352
353 // read config, or use default one
354 let entity_exists = config_file.exists();
355 match (entity_exists, user_custom_file) {
356 (false, false) => Ok(Self::new()),
357 _ => {
358 let contents = fs::read_to_string(&config_file).with_context(|| {
359 format!("failed to read config file: {}", config_file.display())
360 })?;
361 let config = toml::from_str::<Config>(&contents).with_context(|| {
362 format!("failed to parse config file: {}", config_file.display())
363 })?;
364 Ok(config.cache)
365 }
366 }
367 }
368
369 generate_setting_getter!(worker_event_queue_size: u64);
370 generate_setting_getter!(baseline_compression_level: i32);
371 generate_setting_getter!(optimized_compression_level: i32);
372 generate_setting_getter!(optimized_compression_usage_counter_threshold: u64);
373 generate_setting_getter!(cleanup_interval: Duration);
374 generate_setting_getter!(optimizing_compression_task_timeout: Duration);
375 generate_setting_getter!(allowed_clock_drift_for_files_from_future: Duration);
376 generate_setting_getter!(file_count_soft_limit: u64);
377 generate_setting_getter!(files_total_size_soft_limit: u64);
378 generate_setting_getter!(file_count_limit_percent_if_deleting: u8);
379 generate_setting_getter!(files_total_size_limit_percent_if_deleting: u8);
380
381 /// Returns path to the cache directory if one is set.
directory(&self) -> Option<&PathBuf>382 pub fn directory(&self) -> Option<&PathBuf> {
383 self.directory.as_ref()
384 }
385
386 /// Specify where the cache directory is. Must be an absolute path.
with_directory(&mut self, directory: impl Into<PathBuf>) -> &mut Self387 pub fn with_directory(&mut self, directory: impl Into<PathBuf>) -> &mut Self {
388 self.directory = Some(directory.into());
389 self
390 }
391
392 /// Size of cache worker event queue. If the queue is full, incoming cache usage events will be
393 /// dropped.
with_worker_event_queue_size(&mut self, size: u64) -> &mut Self394 pub fn with_worker_event_queue_size(&mut self, size: u64) -> &mut Self {
395 self.worker_event_queue_size = size;
396 self
397 }
398
399 /// Compression level used when a new cache file is being written by the cache system. Wasmtime
400 /// uses zstd compression.
with_baseline_compression_level(&mut self, level: i32) -> &mut Self401 pub fn with_baseline_compression_level(&mut self, level: i32) -> &mut Self {
402 self.baseline_compression_level = level;
403 self
404 }
405
406 /// Compression level used when the cache worker decides to recompress a cache file. Wasmtime
407 /// uses zstd compression.
with_optimized_compression_level(&mut self, level: i32) -> &mut Self408 pub fn with_optimized_compression_level(&mut self, level: i32) -> &mut Self {
409 self.optimized_compression_level = level;
410 self
411 }
412
413 /// One of the conditions for the cache worker to recompress a cache file is to have usage
414 /// count of the file exceeding this threshold.
with_optimized_compression_usage_counter_threshold( &mut self, threshold: u64, ) -> &mut Self415 pub fn with_optimized_compression_usage_counter_threshold(
416 &mut self,
417 threshold: u64,
418 ) -> &mut Self {
419 self.optimized_compression_usage_counter_threshold = threshold;
420 self
421 }
422
423 /// When the cache worker is notified about a cache file being updated by the cache system and
424 /// this interval has already passed since last cleaning up, the worker will attempt a new
425 /// cleanup.
with_cleanup_interval(&mut self, interval: Duration) -> &mut Self426 pub fn with_cleanup_interval(&mut self, interval: Duration) -> &mut Self {
427 self.cleanup_interval = interval;
428 self
429 }
430
431 /// When the cache worker decides to recompress a cache file, it makes sure that no other
432 /// worker has started the task for this file within the last
433 /// optimizing-compression-task-timeout interval. If some worker has started working on it,
434 /// other workers are skipping this task.
with_optimizing_compression_task_timeout(&mut self, timeout: Duration) -> &mut Self435 pub fn with_optimizing_compression_task_timeout(&mut self, timeout: Duration) -> &mut Self {
436 self.optimizing_compression_task_timeout = timeout;
437 self
438 }
439
440 /// ### Locks
441 ///
442 /// When the cache worker attempts acquiring a lock for some task, it checks if some other
443 /// worker has already acquired such a lock. To be fault tolerant and eventually execute every
444 /// task, the locks expire after some interval. However, because of clock drifts and different
445 /// timezones, it would happen that some lock was created in the future. This setting defines a
446 /// tolerance limit for these locks. If the time has been changed in the system (i.e. two years
447 /// backwards), the cache system should still work properly. Thus, these locks will be treated
448 /// as expired (assuming the tolerance is not too big).
449 ///
450 /// ### Cache files
451 ///
452 /// Similarly to the locks, the cache files or their metadata might have modification time in
453 /// distant future. The cache system tries to keep these files as long as possible. If the
454 /// limits are not reached, the cache files will not be deleted. Otherwise, they will be
455 /// treated as the oldest files, so they might survive. If the user actually uses the cache
456 /// file, the modification time will be updated.
with_allowed_clock_drift_for_files_from_future(&mut self, drift: Duration) -> &mut Self457 pub fn with_allowed_clock_drift_for_files_from_future(&mut self, drift: Duration) -> &mut Self {
458 self.allowed_clock_drift_for_files_from_future = drift;
459 self
460 }
461
462 /// Soft limit for the file count in the cache directory.
463 ///
464 /// This doesn't include files with metadata. To learn more, please refer to the cache system
465 /// section.
with_file_count_soft_limit(&mut self, limit: u64) -> &mut Self466 pub fn with_file_count_soft_limit(&mut self, limit: u64) -> &mut Self {
467 self.file_count_soft_limit = limit;
468 self
469 }
470
471 /// Soft limit for the total size* of files in the cache directory.
472 ///
473 /// This doesn't include files with metadata. To learn more, please refer to the cache system
474 /// section.
475 ///
476 /// *this is the file size, not the space physically occupied on the disk.
with_files_total_size_soft_limit(&mut self, limit: u64) -> &mut Self477 pub fn with_files_total_size_soft_limit(&mut self, limit: u64) -> &mut Self {
478 self.files_total_size_soft_limit = limit;
479 self
480 }
481
482 /// If file-count-soft-limit is exceeded and the cache worker performs the cleanup task, then
483 /// the worker will delete some cache files, so after the task, the file count should not
484 /// exceed file-count-soft-limit * file-count-limit-percent-if-deleting.
485 ///
486 /// This doesn't include files with metadata. To learn more, please refer to the cache system
487 /// section.
with_file_count_limit_percent_if_deleting(&mut self, percent: u8) -> &mut Self488 pub fn with_file_count_limit_percent_if_deleting(&mut self, percent: u8) -> &mut Self {
489 self.file_count_limit_percent_if_deleting = percent;
490 self
491 }
492
493 /// If files-total-size-soft-limit is exceeded and cache worker performs the cleanup task, then
494 /// the worker will delete some cache files, so after the task, the files total size should not
495 /// exceed files-total-size-soft-limit * files-total-size-limit-percent-if-deleting.
496 ///
497 /// This doesn't include files with metadata. To learn more, please refer to the cache system
498 /// section.
with_files_total_size_limit_percent_if_deleting(&mut self, percent: u8) -> &mut Self499 pub fn with_files_total_size_limit_percent_if_deleting(&mut self, percent: u8) -> &mut Self {
500 self.files_total_size_limit_percent_if_deleting = percent;
501 self
502 }
503
504 /// validate values and fill in defaults
validate(&mut self) -> Result<()>505 pub(crate) fn validate(&mut self) -> Result<()> {
506 self.validate_directory_or_default()?;
507 self.validate_worker_event_queue_size();
508 self.validate_baseline_compression_level()?;
509 self.validate_optimized_compression_level()?;
510 self.validate_file_count_limit_percent_if_deleting()?;
511 self.validate_files_total_size_limit_percent_if_deleting()?;
512 Ok(())
513 }
514
validate_directory_or_default(&mut self) -> Result<()>515 fn validate_directory_or_default(&mut self) -> Result<()> {
516 if self.directory.is_none() {
517 match project_dirs() {
518 Some(proj_dirs) => self.directory = Some(proj_dirs.cache_dir().to_path_buf()),
519 None => {
520 bail!("Cache directory not specified and failed to get the default");
521 }
522 }
523 }
524
525 // On Windows, if we want long paths, we need '\\?\' prefix, but it doesn't work
526 // with relative paths. One way to get absolute path (the only one?) is to use
527 // fs::canonicalize, but it requires that given path exists. The extra advantage
528 // of this method is fact that the method prepends '\\?\' on Windows.
529 let cache_dir = self.directory.as_ref().unwrap();
530
531 if !cache_dir.is_absolute() {
532 bail!(
533 "Cache directory path has to be absolute, path: {}",
534 cache_dir.display(),
535 );
536 }
537
538 fs::create_dir_all(cache_dir).with_context(|| {
539 format!("failed to create cache directory: {}", cache_dir.display())
540 })?;
541 let canonical = fs::canonicalize(cache_dir).with_context(|| {
542 format!(
543 "failed to canonicalize cache directory: {}",
544 cache_dir.display()
545 )
546 })?;
547 self.directory = Some(canonical);
548 Ok(())
549 }
550
validate_worker_event_queue_size(&self)551 fn validate_worker_event_queue_size(&self) {
552 if self.worker_event_queue_size < worker_event_queue_size_warning_threshold() {
553 warn!("Detected small worker event queue size. Some messages might be lost.");
554 }
555 }
556
validate_baseline_compression_level(&self) -> Result<()>557 fn validate_baseline_compression_level(&self) -> Result<()> {
558 if !ZSTD_COMPRESSION_LEVELS.contains(&self.baseline_compression_level) {
559 bail!(
560 "Invalid baseline compression level: {} not in {:#?}",
561 self.baseline_compression_level,
562 ZSTD_COMPRESSION_LEVELS
563 );
564 }
565 Ok(())
566 }
567
568 // assumption: baseline compression level has been verified
validate_optimized_compression_level(&self) -> Result<()>569 fn validate_optimized_compression_level(&self) -> Result<()> {
570 if !ZSTD_COMPRESSION_LEVELS.contains(&self.optimized_compression_level) {
571 bail!(
572 "Invalid optimized compression level: {} not in {:#?}",
573 self.optimized_compression_level,
574 ZSTD_COMPRESSION_LEVELS
575 );
576 }
577
578 if self.optimized_compression_level < self.baseline_compression_level {
579 bail!(
580 "Invalid optimized compression level is lower than baseline: {} < {}",
581 self.optimized_compression_level,
582 self.baseline_compression_level
583 );
584 }
585 Ok(())
586 }
587
validate_file_count_limit_percent_if_deleting(&self) -> Result<()>588 fn validate_file_count_limit_percent_if_deleting(&self) -> Result<()> {
589 if self.file_count_limit_percent_if_deleting > 100 {
590 bail!(
591 "Invalid files count limit percent if deleting: {} not in range 0-100%",
592 self.file_count_limit_percent_if_deleting
593 );
594 }
595 Ok(())
596 }
597
validate_files_total_size_limit_percent_if_deleting(&self) -> Result<()>598 fn validate_files_total_size_limit_percent_if_deleting(&self) -> Result<()> {
599 if self.files_total_size_limit_percent_if_deleting > 100 {
600 bail!(
601 "Invalid files total size limit percent if deleting: {} not in range 0-100%",
602 self.files_total_size_limit_percent_if_deleting
603 );
604 }
605 Ok(())
606 }
607 }
608
609 #[cfg(test)]
610 #[macro_use]
611 pub mod tests;
612