|
|
… |
5 unchanged lines |
| 6 |
6 |
|
use crate::home::{Home, RunnerConfig, RunnerKey}; |
| 7 |
7 |
|
use crate::{microvm, workspace}; |
| 8 |
8 |
|
use anyhow::{bail, Context, Result}; |
| 9 |
|
- |
use flow_core::api::{Claim, ClaimRequest, Finish, LogBatch, Record, Register, Registered, RunnerInfo, MAX_RECORD_TEXT}; |
|
9 |
+ |
use flow_core::api::{Claim, ClaimRequest, Decline, Finish, LogBatch, Record, Register, Registered, RunnerInfo, MAX_RECORD_TEXT}; |
| 10 |
10 |
|
use flow_core::{scope_tree_hash, Receipt}; |
| 11 |
11 |
|
use oxy_core::hash::ChangeHash; |
| 12 |
12 |
|
use oxy_core::repo::Repo; |
|
|
… |
76 unchanged lines |
| 89 |
89 |
|
} |
| 90 |
90 |
|
} |
| 91 |
91 |
|
|
|
92 |
+ |
/// Hand job `job` back, saying why this runner will not run it. |
|
93 |
+ |
pub fn decline(&self, job: i64, reason: &str) -> Result<()> { |
|
94 |
+ |
self.post(&format!("/flow/jobs/{job}/decline"), &Decline { reason: reason.into() })?; |
|
95 |
+ |
Ok(()) |
|
96 |
+ |
} |
|
97 |
+ |
|
| 92 |
98 |
|
pub fn log(&self, job: i64, records: Vec<Record>) -> Result<()> { |
| 93 |
99 |
|
self.post(&format!("/flow/jobs/{job}/log"), &LogBatch { records })?; |
| 94 |
100 |
|
Ok(()) |
|
|
… |
209 unchanged lines |
| 304 |
310 |
|
return Err(format!("no isolation tier can run here: {}", missing.join("; "))); |
| 305 |
311 |
|
} |
| 306 |
312 |
|
Ok((have, missing)) |
|
313 |
+ |
} |
|
314 |
+ |
|
|
315 |
+ |
/// Why this runner cannot run `claim` now (spec §11.5), checked before |
|
316 |
+ |
/// anything is fetched: the tier, the architecture, the image and the |
|
317 |
+ |
/// memory. The hub chose it from what the runner last said, which may be |
|
318 |
+ |
/// out of date. |
|
319 |
+ |
fn preflight(home: &Home, claim: &Claim) -> Option<String> { |
|
320 |
+ |
let sb = &claim.sandbox; |
|
321 |
+ |
match sb.runtime { |
|
322 |
+ |
flow_core::Runtime::Microvm => { |
|
323 |
+ |
if let Err(e) = std::fs::OpenOptions::new().read(true).write(true).open("/dev/kvm") { |
|
324 |
+ |
return Some(format!("the MicroVM tier needs /dev/kvm, which cannot be opened here ({e})")); |
|
325 |
+ |
} |
|
326 |
+ |
if home.config.microvm.is_none() { |
|
327 |
+ |
return Some("the MicroVM tier is not set up here (flow.toml has no [microvm] section)".into()); |
|
328 |
+ |
} |
|
329 |
+ |
} |
|
330 |
+ |
flow_core::Runtime::Syscall => { |
|
331 |
+ |
if let Err(e) = crate::syscall::available() { |
|
332 |
+ |
return Some(format!("the syscall tier cannot run here: {e}")); |
|
333 |
+ |
} |
|
334 |
+ |
} |
|
335 |
+ |
flow_core::Runtime::Wasm => return Some("this runner has no wasm tier".into()), |
|
336 |
+ |
} |
|
337 |
+ |
sandbox_problem(home, sb, std::env::consts::ARCH, available_memory_mib()) |
|
338 |
+ |
} |
|
339 |
+ |
|
|
340 |
+ |
/// What stops `sb` running on this home's images, on a machine of |
|
341 |
+ |
/// architecture `here` with `free_mib` of memory free. |
|
342 |
+ |
fn sandbox_problem(home: &Home, sb: &flow_core::Sandbox, here: &str, free_mib: Option<u64>) -> Option<String> { |
|
343 |
+ |
if let Some(want) = sb.arch.as_deref().filter(|a| *a != here) { |
|
344 |
+ |
return Some(format!("the sandbox needs {want}; this runner is {here}")); |
|
345 |
+ |
} |
|
346 |
+ |
let short = &sb.rootfs[..12.min(sb.rootfs.len())]; |
|
347 |
+ |
let image = home.rootfs(&sb.rootfs); |
|
348 |
+ |
if !image.is_file() { |
|
349 |
+ |
return Some(format!("rootfs {short} is not on this runner")); |
|
350 |
+ |
} |
|
351 |
+ |
if let Some(arch) = home.image_arch(&sb.rootfs).filter(|a| a != here) { |
|
352 |
+ |
return Some(format!("rootfs {short} is for {arch}; this runner is {here}")); |
|
353 |
+ |
} |
|
354 |
+ |
if let Err(e) = home.verified(&image, &sb.rootfs) { |
|
355 |
+ |
return Some(format!("rootfs {short} does not verify here: {e:#}")); |
|
356 |
+ |
} |
|
357 |
+ |
if let Some(free) = free_mib.filter(|free| *free < u64::from(sb.memory_mib)) { |
|
358 |
+ |
return Some(format!("only {free} MiB of memory is free here; the sandbox needs {} MiB", sb.memory_mib)); |
|
359 |
+ |
} |
|
360 |
+ |
None |
|
361 |
+ |
} |
|
362 |
+ |
|
|
363 |
+ |
/// `MemAvailable` from /proc/meminfo, in MiB. |
|
364 |
+ |
fn available_memory_mib() -> Option<u64> { |
|
365 |
+ |
let info = std::fs::read_to_string("/proc/meminfo").ok()?; |
|
366 |
+ |
let kib: u64 = info.lines().find_map(|l| l.strip_prefix("MemAvailable:"))?.trim().trim_end_matches("kB").trim().parse().ok()?; |
|
367 |
+ |
Some(kib / 1024) |
| 307 |
368 |
|
} |
| 308 |
369 |
|
|
| 309 |
370 |
|
pub fn serve(home: &Home) -> Result<()> { |
|
|
… |
66 unchanged lines |
| 376 |
437 |
|
let (home, hub, cfg, used, key, held) = (home, &hub, &cfg, &used, &key, &held); |
| 377 |
438 |
|
scope.spawn(move || { |
| 378 |
439 |
|
let id = claim.job_id; |
|
440 |
+ |
if let Some(why) = preflight(home, &claim) { |
|
441 |
+ |
eprintln!("job {id}: declined: {why}"); |
|
442 |
+ |
if let Err(e) = hub.decline(id, &why) { |
|
443 |
+ |
eprintln!("job {id}: decline: {e:#}"); |
|
444 |
+ |
} |
|
445 |
+ |
held.lock().unwrap().remove(&id); |
|
446 |
+ |
let mut u = used.lock().unwrap(); |
|
447 |
+ |
u.jobs -= 1; |
|
448 |
+ |
u.memory_mib -= mem; |
|
449 |
+ |
u.cpus -= cpus; |
|
450 |
+ |
return; |
|
451 |
+ |
} |
| 379 |
452 |
|
eprintln!( |
| 380 |
453 |
|
"job {id}: {} {} on {}", |
| 381 |
454 |
|
claim.repo, |
|
|
… |
355 unchanged lines |
| 737 |
810 |
|
#[cfg(test)] |
| 738 |
811 |
|
mod tests { |
| 739 |
812 |
|
use super::*; |
|
813 |
+ |
|
|
814 |
+ |
#[test] |
|
815 |
+ |
fn a_runner_names_what_stops_a_sandbox() { |
|
816 |
+ |
let dir = tempfile::tempdir().unwrap(); |
|
817 |
+ |
let home = Home::locate(Some(dir.path())).unwrap(); |
|
818 |
+ |
let bytes = b"not really ext4"; |
|
819 |
+ |
let hash = blake3::hash(bytes).to_hex().to_string(); |
|
820 |
+ |
let mut sb: flow_core::Sandbox = serde_json::from_value(serde_json::json!({ |
|
821 |
+ |
"name": "s", "runtime": "syscall", "rootfs": "ab".repeat(32), "memory_mib": 1024, "cpus": 1, "network": "isolated" |
|
822 |
+ |
})) |
|
823 |
+ |
.unwrap(); |
|
824 |
+ |
let problem = |sb: &flow_core::Sandbox, free| sandbox_problem(&home, sb, "x86_64", free); |
|
825 |
+ |
assert_eq!(problem(&sb, None).unwrap(), "rootfs abababababab is not on this runner"); |
|
826 |
+ |
std::fs::write(home.rootfs(&hash), bytes).unwrap(); |
|
827 |
+ |
sb.rootfs = hash.clone(); |
|
828 |
+ |
assert_eq!(problem(&sb, Some(4096)), None); |
|
829 |
+ |
assert_eq!(problem(&sb, Some(512)).unwrap(), "only 512 MiB of memory is free here; the sandbox needs 1024 MiB"); |
|
830 |
+ |
std::fs::write(home.image_manifest(&hash), r#"{"arch":"riscv64"}"#).unwrap(); |
|
831 |
+ |
assert_eq!(problem(&sb, None).unwrap(), format!("rootfs {} is for riscv64; this runner is x86_64", &hash[..12])); |
|
832 |
+ |
sb.arch = Some("aarch64".into()); |
|
833 |
+ |
assert_eq!(problem(&sb, None).unwrap(), "the sandbox needs aarch64; this runner is x86_64"); |
|
834 |
+ |
sb.arch = None; |
|
835 |
+ |
std::fs::remove_file(home.image_manifest(&hash)).unwrap(); |
|
836 |
+ |
std::fs::write(home.rootfs(&hash), b"changed").unwrap(); |
|
837 |
+ |
assert!(problem(&sb, None).unwrap().contains("does not verify")); |
|
838 |
+ |
} |
| 740 |
839 |
|
|
| 741 |
840 |
|
#[test] |
| 742 |
841 |
|
fn transient_network_errors_are_tried_again_and_others_are_not() { |
|
|
… |
187 unchanged lines |