Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bytecodealliance
GitHub Repository: bytecodealliance/wasmtime
Path: blob/main/crates/cache/src/config.rs
1693 views
1
//! Module for configuring the cache system.
2
3
use anyhow::{Context, Result, anyhow, bail};
4
use directories_next::ProjectDirs;
5
use log::{trace, warn};
6
use serde::{
7
Deserialize,
8
de::{self, Deserializer},
9
};
10
use std::fmt::Debug;
11
use std::fs;
12
use std::path::{Path, PathBuf};
13
use std::time::Duration;
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 {
95
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.
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(|| anyhow!("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
171
const fn default_worker_event_queue_size() -> u64 {
172
0x10
173
}
174
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
179
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
184
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
189
const fn default_optimized_compression_usage_counter_threshold() -> u64 {
190
0x100
191
}
192
// if changed, update cli-cache.md
193
const fn default_cleanup_interval() -> Duration {
194
Duration::from_secs(60 * 60)
195
}
196
// if changed, update cli-cache.md
197
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
203
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
207
const fn default_file_count_soft_limit() -> u64 {
208
0x10_000
209
}
210
// if changed, update cli-cache.md
211
const fn default_files_total_size_soft_limit() -> u64 {
212
1024 * 1024 * 512
213
}
214
// if changed, update cli-cache.md
215
const fn default_file_count_limit_percent_if_deleting() -> u8 {
216
70
217
}
218
// if changed, update cli-cache.md
219
const fn default_files_total_size_limit_percent_if_deleting() -> u8 {
220
70
221
}
222
223
fn project_dirs() -> Option<ProjectDirs> {
224
ProjectDirs::from("", "BytecodeAlliance", "wasmtime")
225
}
226
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.
319
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
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
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).context(format!(
359
"failed to read config file: {}",
360
config_file.display()
361
))?;
362
let config = toml::from_str::<Config>(&contents).context(format!(
363
"failed to parse config file: {}",
364
config_file.display()
365
))?;
366
Ok(config.cache)
367
}
368
}
369
}
370
371
generate_setting_getter!(worker_event_queue_size: u64);
372
generate_setting_getter!(baseline_compression_level: i32);
373
generate_setting_getter!(optimized_compression_level: i32);
374
generate_setting_getter!(optimized_compression_usage_counter_threshold: u64);
375
generate_setting_getter!(cleanup_interval: Duration);
376
generate_setting_getter!(optimizing_compression_task_timeout: Duration);
377
generate_setting_getter!(allowed_clock_drift_for_files_from_future: Duration);
378
generate_setting_getter!(file_count_soft_limit: u64);
379
generate_setting_getter!(files_total_size_soft_limit: u64);
380
generate_setting_getter!(file_count_limit_percent_if_deleting: u8);
381
generate_setting_getter!(files_total_size_limit_percent_if_deleting: u8);
382
383
/// Returns path to the cache directory if one is set.
384
pub fn directory(&self) -> Option<&PathBuf> {
385
self.directory.as_ref()
386
}
387
388
/// Specify where the cache directory is. Must be an absolute path.
389
pub fn with_directory(&mut self, directory: impl Into<PathBuf>) -> &mut Self {
390
self.directory = Some(directory.into());
391
self
392
}
393
394
/// Size of cache worker event queue. If the queue is full, incoming cache usage events will be
395
/// dropped.
396
pub fn with_worker_event_queue_size(&mut self, size: u64) -> &mut Self {
397
self.worker_event_queue_size = size;
398
self
399
}
400
401
/// Compression level used when a new cache file is being written by the cache system. Wasmtime
402
/// uses zstd compression.
403
pub fn with_baseline_compression_level(&mut self, level: i32) -> &mut Self {
404
self.baseline_compression_level = level;
405
self
406
}
407
408
/// Compression level used when the cache worker decides to recompress a cache file. Wasmtime
409
/// uses zstd compression.
410
pub fn with_optimized_compression_level(&mut self, level: i32) -> &mut Self {
411
self.optimized_compression_level = level;
412
self
413
}
414
415
/// One of the conditions for the cache worker to recompress a cache file is to have usage
416
/// count of the file exceeding this threshold.
417
pub fn with_optimized_compression_usage_counter_threshold(
418
&mut self,
419
threshold: u64,
420
) -> &mut Self {
421
self.optimized_compression_usage_counter_threshold = threshold;
422
self
423
}
424
425
/// When the cache worker is notified about a cache file being updated by the cache system and
426
/// this interval has already passed since last cleaning up, the worker will attempt a new
427
/// cleanup.
428
pub fn with_cleanup_interval(&mut self, interval: Duration) -> &mut Self {
429
self.cleanup_interval = interval;
430
self
431
}
432
433
/// When the cache worker decides to recompress a cache file, it makes sure that no other
434
/// worker has started the task for this file within the last
435
/// optimizing-compression-task-timeout interval. If some worker has started working on it,
436
/// other workers are skipping this task.
437
pub fn with_optimizing_compression_task_timeout(&mut self, timeout: Duration) -> &mut Self {
438
self.optimizing_compression_task_timeout = timeout;
439
self
440
}
441
442
/// ### Locks
443
///
444
/// When the cache worker attempts acquiring a lock for some task, it checks if some other
445
/// worker has already acquired such a lock. To be fault tolerant and eventually execute every
446
/// task, the locks expire after some interval. However, because of clock drifts and different
447
/// timezones, it would happen that some lock was created in the future. This setting defines a
448
/// tolerance limit for these locks. If the time has been changed in the system (i.e. two years
449
/// backwards), the cache system should still work properly. Thus, these locks will be treated
450
/// as expired (assuming the tolerance is not too big).
451
///
452
/// ### Cache files
453
///
454
/// Similarly to the locks, the cache files or their metadata might have modification time in
455
/// distant future. The cache system tries to keep these files as long as possible. If the
456
/// limits are not reached, the cache files will not be deleted. Otherwise, they will be
457
/// treated as the oldest files, so they might survive. If the user actually uses the cache
458
/// file, the modification time will be updated.
459
pub fn with_allowed_clock_drift_for_files_from_future(&mut self, drift: Duration) -> &mut Self {
460
self.allowed_clock_drift_for_files_from_future = drift;
461
self
462
}
463
464
/// Soft limit for the file count in the cache directory.
465
///
466
/// This doesn't include files with metadata. To learn more, please refer to the cache system
467
/// section.
468
pub fn with_file_count_soft_limit(&mut self, limit: u64) -> &mut Self {
469
self.file_count_soft_limit = limit;
470
self
471
}
472
473
/// Soft limit for the total size* of files in the cache directory.
474
///
475
/// This doesn't include files with metadata. To learn more, please refer to the cache system
476
/// section.
477
///
478
/// *this is the file size, not the space physically occupied on the disk.
479
pub fn with_files_total_size_soft_limit(&mut self, limit: u64) -> &mut Self {
480
self.files_total_size_soft_limit = limit;
481
self
482
}
483
484
/// If file-count-soft-limit is exceeded and the cache worker performs the cleanup task, then
485
/// the worker will delete some cache files, so after the task, the file count should not
486
/// exceed file-count-soft-limit * file-count-limit-percent-if-deleting.
487
///
488
/// This doesn't include files with metadata. To learn more, please refer to the cache system
489
/// section.
490
pub fn with_file_count_limit_percent_if_deleting(&mut self, percent: u8) -> &mut Self {
491
self.file_count_limit_percent_if_deleting = percent;
492
self
493
}
494
495
/// If files-total-size-soft-limit is exceeded and cache worker performs the cleanup task, then
496
/// the worker will delete some cache files, so after the task, the files total size should not
497
/// exceed files-total-size-soft-limit * files-total-size-limit-percent-if-deleting.
498
///
499
/// This doesn't include files with metadata. To learn more, please refer to the cache system
500
/// section.
501
pub fn with_files_total_size_limit_percent_if_deleting(&mut self, percent: u8) -> &mut Self {
502
self.files_total_size_limit_percent_if_deleting = percent;
503
self
504
}
505
506
/// validate values and fill in defaults
507
pub(crate) fn validate(&mut self) -> Result<()> {
508
self.validate_directory_or_default()?;
509
self.validate_worker_event_queue_size();
510
self.validate_baseline_compression_level()?;
511
self.validate_optimized_compression_level()?;
512
self.validate_file_count_limit_percent_if_deleting()?;
513
self.validate_files_total_size_limit_percent_if_deleting()?;
514
Ok(())
515
}
516
517
fn validate_directory_or_default(&mut self) -> Result<()> {
518
if self.directory.is_none() {
519
match project_dirs() {
520
Some(proj_dirs) => self.directory = Some(proj_dirs.cache_dir().to_path_buf()),
521
None => {
522
bail!("Cache directory not specified and failed to get the default");
523
}
524
}
525
}
526
527
// On Windows, if we want long paths, we need '\\?\' prefix, but it doesn't work
528
// with relative paths. One way to get absolute path (the only one?) is to use
529
// fs::canonicalize, but it requires that given path exists. The extra advantage
530
// of this method is fact that the method prepends '\\?\' on Windows.
531
let cache_dir = self.directory.as_ref().unwrap();
532
533
if !cache_dir.is_absolute() {
534
bail!(
535
"Cache directory path has to be absolute, path: {}",
536
cache_dir.display(),
537
);
538
}
539
540
fs::create_dir_all(cache_dir).context(format!(
541
"failed to create cache directory: {}",
542
cache_dir.display()
543
))?;
544
let canonical = fs::canonicalize(cache_dir).context(format!(
545
"failed to canonicalize cache directory: {}",
546
cache_dir.display()
547
))?;
548
self.directory = Some(canonical);
549
Ok(())
550
}
551
552
fn validate_worker_event_queue_size(&self) {
553
if self.worker_event_queue_size < worker_event_queue_size_warning_threshold() {
554
warn!("Detected small worker event queue size. Some messages might be lost.");
555
}
556
}
557
558
fn validate_baseline_compression_level(&self) -> Result<()> {
559
if !ZSTD_COMPRESSION_LEVELS.contains(&self.baseline_compression_level) {
560
bail!(
561
"Invalid baseline compression level: {} not in {:#?}",
562
self.baseline_compression_level,
563
ZSTD_COMPRESSION_LEVELS
564
);
565
}
566
Ok(())
567
}
568
569
// assumption: baseline compression level has been verified
570
fn validate_optimized_compression_level(&self) -> Result<()> {
571
if !ZSTD_COMPRESSION_LEVELS.contains(&self.optimized_compression_level) {
572
bail!(
573
"Invalid optimized compression level: {} not in {:#?}",
574
self.optimized_compression_level,
575
ZSTD_COMPRESSION_LEVELS
576
);
577
}
578
579
if self.optimized_compression_level < self.baseline_compression_level {
580
bail!(
581
"Invalid optimized compression level is lower than baseline: {} < {}",
582
self.optimized_compression_level,
583
self.baseline_compression_level
584
);
585
}
586
Ok(())
587
}
588
589
fn validate_file_count_limit_percent_if_deleting(&self) -> Result<()> {
590
if self.file_count_limit_percent_if_deleting > 100 {
591
bail!(
592
"Invalid files count limit percent if deleting: {} not in range 0-100%",
593
self.file_count_limit_percent_if_deleting
594
);
595
}
596
Ok(())
597
}
598
599
fn validate_files_total_size_limit_percent_if_deleting(&self) -> Result<()> {
600
if self.files_total_size_limit_percent_if_deleting > 100 {
601
bail!(
602
"Invalid files total size limit percent if deleting: {} not in range 0-100%",
603
self.files_total_size_limit_percent_if_deleting
604
);
605
}
606
Ok(())
607
}
608
}
609
610
#[cfg(test)]
611
#[macro_use]
612
pub mod tests;
613
614