atagen

roundabout

public
Star0
Watch0
e50aca5650c22545f0d99db926ab1ae120f24699
1
ssh://brewery@pls.lobotomise.me:33/atagen/roundabout.git
src/config.rsRust380 lines9.5 KB
1
use std::{collections::BTreeMap, fs, path::PathBuf};
2
3
use serde::Deserialize;
4
5
use crate::{
6
    paths,
7
    units::{Amount, Bytes, Dur},
8
};
9
10
#[derive(Debug, Deserialize)]
11
#[serde(default, deny_unknown_fields)]
12
pub struct Config {
13
    pub budget: Budget,
14
    pub heavy: Heavy,
15
    pub sizes: BTreeMap<String, Preset>,
16
    pub queue: Queue,
17
    pub overrun: Overrun,
18
    pub enforce: Enforce,
19
    pub artifacts: Artifacts,
20
    pub nix: Nix,
21
    pub detect: Detect,
22
}
23
24
#[derive(Debug, Deserialize)]
25
#[serde(default, deny_unknown_fields)]
26
pub struct Budget {
27
    pub ram: Amount<Bytes>,
28
    pub ram_reserve: Bytes,
29
    pub cores: Amount<u32>,
30
    pub disk_reserve: Bytes,
31
    /// every lease's disk estimate is charged to all of these as well as its
32
    /// cwd. pessimistic when they share a fs, which is the point
33
    pub disk_paths: Vec<PathBuf>,
34
    /// memory psi `some avg10`. only gates heavies
35
    pub psi_max: Option<f64>,
36
}
37
38
#[derive(Debug, Deserialize)]
39
#[serde(default, deny_unknown_fields)]
40
pub struct Heavy {
41
    pub ram: Bytes,
42
    pub cores: Amount<u32>,
43
    pub disk: Bytes,
44
}
45
46
#[derive(Clone, Copy, Debug, Deserialize)]
47
#[serde(deny_unknown_fields)]
48
pub struct Preset {
49
    pub ram: Bytes,
50
    pub disk: Bytes,
51
    pub cores: Amount<u32>,
52
    pub time: Dur,
53
}
54
55
#[derive(Debug, Deserialize)]
56
#[serde(default, deny_unknown_fields)]
57
pub struct Queue {
58
    pub linger: Dur,
59
    pub backfill_max_time: Dur,
60
    pub max_wait: Option<Dur>,
61
}
62
63
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
64
#[serde(rename_all = "lowercase")]
65
pub enum RamOverrun {
66
    Warn,
67
    Throttle,
68
}
69
70
#[derive(Debug, Deserialize)]
71
#[serde(default, deny_unknown_fields)]
72
pub struct Overrun {
73
    pub ram: RamOverrun,
74
    /// multiple of the ram estimate
75
    pub ram_slack: f64,
76
    /// multiple of the time estimate
77
    pub time_grace: f64,
78
    pub remind_every: Dur,
79
}
80
81
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
82
#[serde(rename_all = "lowercase")]
83
pub enum Unwrapped {
84
    Deny,
85
    Wrap,
86
    Allow,
87
}
88
89
#[derive(Debug, Deserialize)]
90
#[serde(default, deny_unknown_fields)]
91
pub struct Enforce {
92
    pub unwrapped: Unwrapped,
93
    pub default_size: String,
94
}
95
96
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
97
#[serde(rename_all = "lowercase")]
98
pub enum OverCap {
99
    Warn,
100
    Block,
101
}
102
103
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
104
#[serde(rename_all = "lowercase")]
105
pub enum StopPolicy {
106
    Block,
107
    Nudge,
108
    Warn,
109
    Off,
110
}
111
112
#[derive(Clone, Debug, Deserialize)]
113
#[serde(deny_unknown_fields)]
114
pub struct Rule {
115
    /// trailing `*` for a prefix match, that's all the glob you get
116
    pub name: String,
117
    /// sibling that must exist, so we don't go deleting someone's `build/`
118
    /// because it had a suggestive name
119
    pub marker: Option<String>,
120
    pub clean: Option<Vec<String>>,
121
}
122
123
#[derive(Debug, Deserialize)]
124
#[serde(default, deny_unknown_fields)]
125
pub struct Artifacts {
126
    pub project_cap: Bytes,
127
    pub global_cap: Bytes,
128
    pub idle_after: Dur,
129
    pub size_ttl: Dur,
130
    pub over_cap: OverCap,
131
    pub stop: StopPolicy,
132
    pub rules: Vec<Rule>,
133
    pub extra_rules: Vec<Rule>,
134
    pub nix_gc: bool,
135
    pub nix_min_free: Bytes,
136
}
137
138
#[derive(Debug, Deserialize)]
139
#[serde(default, deny_unknown_fields)]
140
pub struct Nix {
141
    pub inject: bool,
142
    pub cores_per_job: u32,
143
}
144
145
#[derive(Debug, Deserialize)]
146
#[serde(default, deny_unknown_fields)]
147
pub struct Detect {
148
    pub commands: Vec<String>,
149
    pub extra: Vec<String>,
150
}
151
152
impl Default for Config {
153
    fn default() -> Self {
154
        let preset = |ram, disk, cores: &str, time| Preset {
155
            ram,
156
            disk,
157
            cores: cores.parse().unwrap_or(Amount::Abs(1)),
158
            time: Dur::secs(time),
159
        };
160
        let sizes = BTreeMap::from([
161
            (
162
                "s".into(),
163
                preset(Bytes::mib(512), Bytes::mib(256), "1", 60),
164
            ),
165
            ("m".into(), preset(Bytes::gib(2), Bytes::gib(1), "4", 300)),
166
            ("l".into(), preset(Bytes::gib(6), Bytes::gib(5), "50%", 900)),
167
            (
168
                "xl".into(),
169
                preset(Bytes::gib(12), Bytes::gib(20), "100%", 2700),
170
            ),
171
        ]);
172
        Self {
173
            budget: Budget::default(),
174
            heavy: Heavy::default(),
175
            sizes,
176
            queue: Queue::default(),
177
            overrun: Overrun::default(),
178
            enforce: Enforce::default(),
179
            artifacts: Artifacts::default(),
180
            nix: Nix::default(),
181
            detect: Detect::default(),
182
        }
183
    }
184
}
185
186
impl Default for Budget {
187
    fn default() -> Self {
188
        Self {
189
            ram: Amount::Percent(75.0),
190
            ram_reserve: Bytes::gib(4),
191
            cores: Amount::Percent(100.0),
192
            disk_reserve: Bytes::gib(20),
193
            disk_paths: vec!["/nix/store".into()],
194
            psi_max: Some(40.0),
195
        }
196
    }
197
}
198
199
impl Default for Heavy {
200
    fn default() -> Self {
201
        Self {
202
            ram: Bytes::gib(4),
203
            cores: Amount::Percent(50.0),
204
            disk: Bytes::gib(5),
205
        }
206
    }
207
}
208
209
impl Default for Queue {
210
    fn default() -> Self {
211
        Self {
212
            linger: Dur::secs(5),
213
            backfill_max_time: Dur::secs(120),
214
            max_wait: None,
215
        }
216
    }
217
}
218
219
impl Default for Overrun {
220
    fn default() -> Self {
221
        Self {
222
            ram: RamOverrun::Warn,
223
            ram_slack: 1.25,
224
            time_grace: 1.5,
225
            remind_every: Dur::secs(300),
226
        }
227
    }
228
}
229
230
impl Default for Enforce {
231
    fn default() -> Self {
232
        Self {
233
            unwrapped: Unwrapped::Deny,
234
            default_size: "m".into(),
235
        }
236
    }
237
}
238
239
impl Default for Artifacts {
240
    fn default() -> Self {
241
        let rule = |name: &str, marker: Option<&str>| Rule {
242
            name: name.into(),
243
            marker: marker.map(Into::into),
244
            clean: None,
245
        };
246
        Self {
247
            project_cap: Bytes::gib(10),
248
            global_cap: Bytes::gib(60),
249
            idle_after: Dur::secs(86_400),
250
            size_ttl: Dur::secs(600),
251
            over_cap: OverCap::Warn,
252
            stop: StopPolicy::Nudge,
253
            rules: vec![
254
                rule("target", Some("Cargo.toml")),
255
                rule("result*", None),
256
                rule("node_modules", Some("package.json")),
257
                rule("zig-out", Some("build.zig")),
258
                rule(".zig-cache", Some("build.zig")),
259
                rule("_build", Some("dune-project")),
260
                rule("dist-newstyle", Some("cabal.project")),
261
            ],
262
            extra_rules: Vec::new(),
263
            nix_gc: false,
264
            nix_min_free: Bytes::gib(30),
265
        }
266
    }
267
}
268
269
impl Default for Nix {
270
    fn default() -> Self {
271
        Self {
272
            inject: true,
273
            cores_per_job: 4,
274
        }
275
    }
276
}
277
278
impl Default for Detect {
279
    fn default() -> Self {
280
        let commands = [
281
            "cargo build",
282
            "cargo b",
283
            "cargo test",
284
            "cargo t",
285
            "cargo check",
286
            "cargo c",
287
            "cargo clippy",
288
            "cargo run",
289
            "cargo r",
290
            "cargo nextest",
291
            "cargo bench",
292
            "cargo install",
293
            "cargo doc",
294
            "nix build",
295
            "nix flake check",
296
            "nix develop",
297
            "nix run",
298
            "nix shell",
299
            "nix-build",
300
            "nix-shell",
301
            "nixos-rebuild",
302
            "home-manager switch",
303
            "home-manager build",
304
            "make",
305
            "gmake",
306
            "cmake --build",
307
            "ninja",
308
            "meson compile",
309
            "go build",
310
            "go test",
311
            "zig build",
312
            "dune build",
313
            "cabal build",
314
            "stack build",
315
            "npm run build",
316
            "npm test",
317
            "npm ci",
318
            "npm install",
319
            "pnpm build",
320
            "pnpm install",
321
            "yarn build",
322
            "bun run build",
323
            "docker build",
324
            "podman build",
325
            "mvn",
326
            "gradle",
327
            "./gradlew",
328
            "bazel build",
329
            "bazel test",
330
        ];
331
        Self {
332
            commands: commands.into_iter().map(Into::into).collect(),
333
            extra: Vec::new(),
334
        }
335
    }
336
}
337
338
impl Config {
339
    pub fn load() -> Result<Self, String> {
340
        let path = paths::config_file();
341
        match fs::read_to_string(&path) {
342
            Ok(text) => Self::parse(&text).map_err(|e| format!("{}: {e}", path.display())),
343
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Self::default()),
344
            Err(e) => Err(format!("{}: {e}", path.display())),
345
        }
346
    }
347
348
    pub fn parse(text: &str) -> Result<Self, toml::de::Error> {
349
        let mut config: Self = toml::from_str(text)?;
350
        for (name, preset) in Self::default().sizes {
351
            config.sizes.entry(name).or_insert(preset);
352
        }
353
        Ok(config)
354
    }
355
356
    pub fn preset(&self, name: &str) -> Result<Preset, String> {
357
        self.sizes.get(name).copied().ok_or_else(|| {
358
            let known: Vec<_> = self.sizes.keys().map(String::as_str).collect();
359
            format!(
360
                "unknown size '{name}', known sizes are {}",
361
                known.join(", ")
362
            )
363
        })
364
    }
365
366
    pub fn artifact_rules(&self) -> impl Iterator<Item = &Rule> {
367
        self.artifacts
368
            .rules
369
            .iter()
370
            .chain(&self.artifacts.extra_rules)
371
    }
372
373
    pub fn detect_patterns(&self) -> impl Iterator<Item = &str> {
374
        self.detect
375
            .commands
376
            .iter()
377
            .chain(&self.detect.extra)
378
            .map(String::as_str)
379
    }
380
}