atagen

roundabout

public
Star0
Watch0
ecc1cf8df0f5b88c6bf2cd07254c98d13827cd26
1
ssh://brewery@pls.lobotomise.me:33/atagen/roundabout.git
src/run.rsRust397 lines11.9 KB
1
use std::{
2
    env,
3
    fmt::Write as _,
4
    os::unix::{net::UnixStream, process::CommandExt},
5
    process::{Command, ExitStatus},
6
    sync::{
7
        Arc,
8
        atomic::{AtomicBool, AtomicI32, Ordering},
9
    },
10
    thread,
11
    time::{Duration, Instant},
12
};
13
14
use crate::{
15
    artifacts, client,
16
    config::{Config, OverCap, RamOverrun},
17
    history, paths,
18
    proto::{self, Acquire, Event, Reader, Request},
19
    sched::Need,
20
    sys,
21
    units::{Amount, Bytes, Dur},
22
};
23
24
pub const EXIT_TIMEOUT: i32 = 124;
25
pub const EXIT_GAVE_UP: i32 = 75;
26
27
#[derive(Default)]
28
pub struct Args {
29
    pub size: Option<String>,
30
    pub ram: Option<Bytes>,
31
    pub disk: Option<Bytes>,
32
    pub cores: Option<Amount<u32>>,
33
    pub time: Option<Dur>,
34
    pub deadline: Option<Dur>,
35
    pub max_wait: Option<Dur>,
36
    pub session: Option<String>,
37
    pub exact: bool,
38
    pub cmd: Vec<String>,
39
}
40
41
static PGID: AtomicI32 = AtomicI32::new(0);
42
43
extern "C" fn forward(sig: libc::c_int) {
44
    let pgid = PGID.load(Ordering::Relaxed);
45
    if pgid > 1 {
46
        unsafe { libc::kill(-pgid, sig) };
47
    }
48
}
49
50
fn say(msg: &str) {
51
    eprintln!("rabt: {msg}");
52
}
53
54
pub fn need(cfg: &Config, a: &Args) -> Result<Need, String> {
55
    let base = cfg.preset(a.size.as_deref().unwrap_or(&cfg.enforce.default_size))?;
56
    Ok(Need {
57
        ram: a.ram.unwrap_or(base.ram),
58
        disk: a.disk.unwrap_or(base.disk),
59
        cores: a.cores.unwrap_or(base.cores).resolve(sys::cores()),
60
        time: a.time.unwrap_or(base.time).0,
61
    })
62
}
63
64
pub fn base(cfg: &Config) -> Result<Need, String> {
65
    let p = cfg.preset(&cfg.enforce.default_size)?;
66
    Ok(Need {
67
        ram: p.ram,
68
        disk: p.disk,
69
        cores: p.cores.resolve(sys::cores()),
70
        time: p.time.0,
71
    })
72
}
73
74
struct Grant {
75
    stream: UnixStream,
76
    lease: u64,
77
    cores: u32,
78
    ram: Bytes,
79
}
80
81
fn acquire(req: &Acquire, max_wait: Option<Dur>) -> Result<Grant, i32> {
82
    let started = Instant::now();
83
    // a daemon on its way out can accept us then hang up, so go round again
84
    for _ in 0..3 {
85
        let mut stream = client::connect_or_spawn().map_err(|e| {
86
            say(&format!("can't reach the daemon: {e}"));
87
            1
88
        })?;
89
        if proto::send(&mut stream, &Request::Acquire(req.clone())).is_err() {
90
            continue;
91
        }
92
        let _ = stream.set_read_timeout(Some(Duration::from_secs(1)));
93
        let mut reader = Reader::new(&stream).map_err(|_| 1)?;
94
        loop {
95
            match reader.recv::<Event>() {
96
                Ok(Some(Event::Granted {
97
                    lease,
98
                    cores,
99
                    ram,
100
                    heavy,
101
                })) => {
102
                    let bonus = cores.saturating_sub(req.need.cores);
103
                    say(&format!(
104
                        "lease {lease} go, cores {cores}{}{}",
105
                        if bonus > 0 {
106
                            format!(" ({bonus} spare)")
107
                        } else {
108
                            String::new()
109
                        },
110
                        if heavy { ", heavy" } else { "" }
111
                    ));
112
                    let _ = stream.set_read_timeout(None);
113
                    return Ok(Grant {
114
                        stream,
115
                        lease,
116
                        cores,
117
                        ram,
118
                    });
119
                }
120
                Ok(Some(Event::Queued {
121
                    lease,
122
                    position,
123
                    reason,
124
                })) => {
125
                    let why = reason.map(|r| format!(", {r}")).unwrap_or_default();
126
                    say(&format!("lease {lease} queued at #{position}{why}"));
127
                }
128
                Ok(Some(Event::Kill)) => {
129
                    say("killed while queued");
130
                    return Err(143);
131
                }
132
                Ok(Some(_)) => {}
133
                Err(e)
134
                    if matches!(
135
                        e.kind(),
136
                        std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
137
                    ) =>
138
                {
139
                    if max_wait.is_some_and(|m| started.elapsed() > m.0) {
140
                        say("gave up waiting");
141
                        return Err(EXIT_GAVE_UP);
142
                    }
143
                }
144
                Ok(None) | Err(_) => break,
145
            }
146
        }
147
    }
148
    say("daemon keeps hanging up");
149
    Err(1)
150
}
151
152
fn has_systemd_run() -> bool {
153
    env::var_os("PATH")
154
        .is_some_and(|p| env::split_paths(&p).any(|d| d.join("systemd-run").exists()))
155
}
156
157
fn command(cfg: &Config, a: &Args, need: &Need, grant: &Grant) -> Command {
158
    let mut argv = a.cmd.clone();
159
    if cfg.overrun.ram == RamOverrun::Throttle && cfg!(target_os = "linux") && has_systemd_run() {
160
        let high = Bytes((need.ram.0 as f64 * cfg.overrun.ram_slack) as u64);
161
        let mut wrapped: Vec<String> = [
162
            "systemd-run",
163
            "--user",
164
            "--scope",
165
            "--quiet",
166
            "--collect",
167
            "-p",
168
        ]
169
        .map(Into::into)
170
        .into();
171
        wrapped.push(format!("MemoryHigh={}", high.0));
172
        wrapped.push("--".into());
173
        wrapped.append(&mut argv);
174
        argv = wrapped;
175
    }
176
177
    let n = grant.cores.to_string();
178
    let mut cmd = Command::new(&argv[0]);
179
    cmd.args(&argv[1..])
180
        .process_group(0)
181
        .env("ROUNDABOUT_LEASE", grant.lease.to_string())
182
        .env("CARGO_BUILD_JOBS", &n)
183
        .env("CMAKE_BUILD_PARALLEL_LEVEL", &n);
184
185
    let make = env::var("MAKEFLAGS").unwrap_or_default();
186
    if !make.contains("-j") {
187
        cmd.env("MAKEFLAGS", format!("-j{n} {make}").trim_end());
188
    }
189
190
    if cfg.nix.inject {
191
        let per = cfg.nix.cores_per_job.max(1);
192
        let jobs = (grant.cores / per).max(1);
193
        let cores = grant.cores.min(per);
194
        let mut nix = env::var("NIX_CONFIG").unwrap_or_default();
195
        let _ = write!(nix, "\ncores = {cores}\nmax-jobs = {jobs}\n");
196
        cmd.env("NIX_CONFIG", nix);
197
    }
198
199
    #[cfg(target_os = "linux")]
200
    unsafe {
201
        cmd.pre_exec(|| {
202
            libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGTERM);
203
            Ok(())
204
        });
205
    }
206
    cmd
207
}
208
209
fn cpu_time() -> Duration {
210
    let mut ru = std::mem::MaybeUninit::<libc::rusage>::uninit();
211
    unsafe {
212
        if libc::getrusage(libc::RUSAGE_CHILDREN, ru.as_mut_ptr()) != 0 {
213
            return Duration::ZERO;
214
        }
215
        let ru = ru.assume_init();
216
        let tv = |t: libc::timeval| {
217
            Duration::from_secs(t.tv_sec as u64) + Duration::from_micros(t.tv_usec as u64)
218
        };
219
        tv(ru.ru_utime) + tv(ru.ru_stime)
220
    }
221
}
222
223
fn code(status: ExitStatus) -> i32 {
224
    use std::os::unix::process::ExitStatusExt;
225
    status
226
        .code()
227
        .or_else(|| status.signal().map(|s| 128 + s))
228
        .unwrap_or(1)
229
}
230
231
pub fn run(cfg: &Config, a: &Args) -> i32 {
232
    if a.cmd.is_empty() {
233
        say("nothing to run, put the command after --");
234
        return 2;
235
    }
236
    if a.size.is_none() && a.ram.is_none() {
237
        let sizes: Vec<_> = cfg.sizes.keys().map(String::as_str).collect();
238
        say(&format!(
239
            "how big is it? pass --size {} or --ram and friends. `rabt suggest -- <cmd>` has a guess",
240
            sizes.join("|")
241
        ));
242
        return 2;
243
    }
244
    let mut need = match need(cfg, a) {
245
        Ok(n) => n,
246
        Err(e) => {
247
            say(&e);
248
            return 2;
249
        }
250
    };
251
    let cwd = env::current_dir().unwrap_or_else(|_| "/".into());
252
    let root = paths::project_root(&cwd);
253
254
    let report = artifacts::check(cfg, Some(&root), false);
255
    if report.over() {
256
        let msg = report.describe(cfg);
257
        if cfg.artifacts.over_cap == OverCap::Block {
258
            say(&format!("{msg}. clean up first, `rabt artifacts gc --cap`"));
259
            return EXIT_GAVE_UP;
260
        }
261
        say(&format!(
262
            "{msg}. `rabt artifacts gc --cap` when you get a sec"
263
        ));
264
    }
265
266
    let req = Acquire {
267
        need,
268
        cwd,
269
        cmd: a.cmd.clone(),
270
        session: a.session.clone(),
271
        deadline: a.deadline,
272
        exact: a.exact,
273
    };
274
275
    let grant = match acquire(&req, a.max_wait.or(cfg.queue.max_wait)) {
276
        Ok(g) => g,
277
        Err(code) => return code,
278
    };
279
    need.ram = need.ram.max(grant.ram);
280
281
    let mut child = match command(cfg, a, &need, &grant).spawn() {
282
        Ok(c) => c,
283
        Err(e) => {
284
            say(&format!("couldn't start {}: {e}", a.cmd[0]));
285
            return 127;
286
        }
287
    };
288
    let pgid = i32::try_from(child.id()).expect("pid fits in pid_t");
289
    PGID.store(pgid, Ordering::Relaxed);
290
    for sig in [libc::SIGINT, libc::SIGTERM, libc::SIGHUP] {
291
        unsafe { libc::signal(sig, forward as *const () as libc::sighandler_t) };
292
    }
293
294
    let mut out = grant.stream.try_clone().ok();
295
    if let Some(s) = &mut out {
296
        let _ = proto::send(s, &Request::Started { pid: pgid });
297
    }
298
299
    let killed = Arc::new(AtomicBool::new(false));
300
    if let Ok(mut reader) = Reader::new(&grant.stream) {
301
        let killed = killed.clone();
302
        thread::spawn(move || {
303
            while let Ok(Some(ev)) = reader.recv::<Event>() {
304
                if matches!(ev, Event::Kill) {
305
                    killed.store(true, Ordering::Relaxed);
306
                }
307
            }
308
        });
309
    }
310
311
    let started = Instant::now();
312
    let grace = need.time.mul_f64(cfg.overrun.time_grace);
313
    let loud_ram = Bytes((need.ram.0 as f64 * cfg.overrun.ram_slack) as u64);
314
    let mut peak = Bytes::ZERO;
315
    let mut last_sample: Option<Instant> = None;
316
    let mut term_sent: Option<Instant> = None;
317
    let mut timed_out = false;
318
    let mut warned_ram = false;
319
    let mut warned_time = false;
320
321
    let status = loop {
322
        match child.try_wait() {
323
            Ok(Some(status)) => break status,
324
            Ok(None) => {}
325
            Err(e) => {
326
                say(&format!("lost track of the child: {e}"));
327
                return 1;
328
            }
329
        }
330
        let elapsed = started.elapsed();
331
332
        if term_sent.is_none() {
333
            if killed.load(Ordering::Relaxed) {
334
                say("killed on request");
335
                term_sent = Some(Instant::now());
336
            } else if a.deadline.is_some_and(|d| elapsed > d.0) {
337
                say("deadline hit, killing it");
338
                timed_out = true;
339
                term_sent = Some(Instant::now());
340
            }
341
            if term_sent.is_some() {
342
                unsafe { libc::kill(-pgid, libc::SIGTERM) };
343
            }
344
        } else if term_sent.is_some_and(|t| t.elapsed() > Duration::from_secs(10)) {
345
            unsafe { libc::kill(-pgid, libc::SIGKILL) };
346
        }
347
348
        if last_sample.is_none_or(|t| t.elapsed() >= Duration::from_secs(2)) {
349
            last_sample = Some(Instant::now());
350
            let rss = sys::group_rss(pgid);
351
            peak = peak.max(rss);
352
            if let Some(s) = &mut out {
353
                let _ = proto::send(s, &Request::Usage { rss });
354
            }
355
            if !warned_ram && rss > loud_ram {
356
                warned_ram = true;
357
                say(&format!("using {rss} against a {} estimate", need.ram));
358
            }
359
        }
360
        if !warned_time && elapsed > grace {
361
            warned_time = true;
362
            say(&format!(
363
                "{} past its {} estimate and still going",
364
                Dur(elapsed),
365
                Dur(need.time)
366
            ));
367
        }
368
        thread::sleep(Duration::from_millis(250));
369
    };
370
371
    let wall = started.elapsed();
372
    if let Some(s) = &mut out {
373
        let _ = proto::send(s, &Request::Done);
374
    }
375
    history::record(
376
        history::key(&root, &a.cmd),
377
        peak,
378
        wall,
379
        cpu_time(),
380
        grant.cores,
381
    );
382
    artifacts::register(cfg, &root, true);
383
    if peak > loud_ram || wall > grace {
384
        say(&format!(
385
            "took {} and peaked at {}, asked for {} and {}. ask for more next time",
386
            Dur(wall),
387
            peak,
388
            Dur(need.time),
389
            need.ram
390
        ));
391
    }
392
    if timed_out {
393
        EXIT_TIMEOUT
394
    } else {
395
        code(status)
396
    }
397
}