|
|
… |
92 unchanged lines |
| 93 |
93 |
|
} |
| 94 |
94 |
|
} |
| 95 |
95 |
|
|
| 96 |
|
- |
pub fn finish(&self, job: i64, f: &Finish) -> Result<()> { |
| 97 |
|
- |
self.post(&format!("/flow/jobs/{job}/finish"), f)?; |
| 98 |
|
- |
Ok(()) |
|
96 |
+ |
/// Report a job's end until the hub has it (oxygenhub#71): while the hub |
|
97 |
+ |
/// cannot be reached or fails (5xx), try again after `first_wait`, |
|
98 |
+ |
/// doubling to a minute, for up to `patience`. A refusal (4xx: the job was ended |
|
99 |
+ |
/// already, say) is final. A result lost here leaves the job running |
|
100 |
+ |
/// on the hub until its time limit. |
|
101 |
+ |
pub fn finish_until_accepted(&self, job: i64, f: &Finish, patience: Duration, first_wait: Duration) -> Result<()> { |
|
102 |
+ |
let url = format!("{}/api/v1/flow/jobs/{job}/finish", self.base); |
|
103 |
+ |
let deadline = std::time::Instant::now() + patience; |
|
104 |
+ |
let mut wait = first_wait; |
|
105 |
+ |
loop { |
|
106 |
+ |
let why = match self.agent.post(&url).set("Authorization", &format!("Bearer {}", self.token)).send_json(f) { |
|
107 |
+ |
Ok(_) => return Ok(()), |
|
108 |
+ |
Err(ureq::Error::Status(code, r)) if code < 500 => { |
|
109 |
+ |
bail!("{url}: HTTP {code}: {}", r.into_string().unwrap_or_default()) |
|
110 |
+ |
} |
|
111 |
+ |
Err(ureq::Error::Status(code, _)) => format!("HTTP {code}"), |
|
112 |
+ |
Err(e) => e.to_string(), |
|
113 |
+ |
}; |
|
114 |
+ |
if std::time::Instant::now() + wait > deadline { |
|
115 |
+ |
bail!("{url}: gave up after {} minutes: {why}", patience.as_secs() / 60); |
|
116 |
+ |
} |
|
117 |
+ |
eprintln!("job {job}: the hub did not take the result ({why}); trying again in {} s", wait.as_secs()); |
|
118 |
+ |
std::thread::sleep(wait); |
|
119 |
+ |
wait = (wait * 2).min(Duration::from_secs(60)); |
|
120 |
+ |
} |
|
121 |
+ |
} |
|
122 |
+ |
} |
|
123 |
+ |
|
|
124 |
+ |
/// How long a runner keeps trying to report a job's end. |
|
125 |
+ |
const FINISH_PATIENCE: Duration = Duration::from_secs(2 * 3600); |
|
126 |
+ |
|
|
127 |
+ |
/// Where a job's result waits until the hub has it, so a runner that |
|
128 |
+ |
/// restarts still reports it. |
|
129 |
+ |
fn pending_path(home: &Home, job: i64) -> PathBuf { |
|
130 |
+ |
home.root.join("pending").join(format!("{job}.json")) |
|
131 |
+ |
} |
|
132 |
+ |
|
|
133 |
+ |
/// Report `f`, keeping it on disk until the hub has it. |
|
134 |
+ |
fn report_finish(home: &Home, hub: &Hub, job: i64, f: &Finish) { |
|
135 |
+ |
let path = pending_path(home, job); |
|
136 |
+ |
let kept = std::fs::create_dir_all(path.parent().unwrap()) |
|
137 |
+ |
.and_then(|_| std::fs::write(&path, serde_json::to_vec(f).unwrap_or_default())) |
|
138 |
+ |
.is_ok(); |
|
139 |
+ |
match hub.finish_until_accepted(job, f, FINISH_PATIENCE, Duration::from_secs(5)) { |
|
140 |
+ |
Ok(()) => {} |
|
141 |
+ |
Err(e) => eprintln!("job {job}: reporting failed: {e:#}"), |
|
142 |
+ |
} |
|
143 |
+ |
if kept { |
|
144 |
+ |
let _ = std::fs::remove_file(&path); |
| 99 |
145 |
|
} |
|
146 |
+ |
} |
|
147 |
+ |
|
|
148 |
+ |
/// Results a previous run of the runner could not report, to send again. |
|
149 |
+ |
fn report_pending(home: &Home) -> Vec<(i64, Finish)> { |
|
150 |
+ |
let Ok(dir) = std::fs::read_dir(home.root.join("pending")) else { |
|
151 |
+ |
return Vec::new(); |
|
152 |
+ |
}; |
|
153 |
+ |
dir.filter_map(|e| { |
|
154 |
+ |
let p = e.ok()?.path(); |
|
155 |
+ |
let job: i64 = p.file_stem()?.to_str()?.parse().ok()?; |
|
156 |
+ |
let f: Finish = serde_json::from_slice(&std::fs::read(&p).ok()?).ok()?; |
|
157 |
+ |
Some((job, f)) |
|
158 |
+ |
}) |
|
159 |
+ |
.collect() |
| 100 |
160 |
|
} |
| 101 |
161 |
|
|
| 102 |
162 |
|
/// `flow runner register`: exchange a one-time registration token from |
|
|
… |
100 unchanged lines |
| 203 |
263 |
|
); |
| 204 |
264 |
|
let used = std::sync::Mutex::new(Used::default()); |
| 205 |
265 |
|
let mut idle = Duration::from_secs(2); |
| 206 |
|
- |
std::thread::scope(|scope| loop { |
| 207 |
|
- |
let room = budget.room(*used.lock().unwrap()); |
| 208 |
|
- |
let Some((free_memory_mib, free_cpus)) = room else { |
| 209 |
|
- |
std::thread::sleep(Duration::from_millis(500)); |
| 210 |
|
- |
continue; |
| 211 |
|
- |
}; |
| 212 |
|
- |
let req = ClaimRequest { |
| 213 |
|
- |
runner: name.clone(), |
| 214 |
|
- |
runtimes: vec!["microvm".into()], |
| 215 |
|
- |
free_memory_mib, |
| 216 |
|
- |
free_cpus, |
| 217 |
|
- |
info: Some(RunnerInfo { |
| 218 |
|
- |
version: env!("OXY_VERSION_LONG").into(), |
| 219 |
|
- |
..RunnerInfo::here(cfg.labels.clone(), budget.max_jobs as u32, cfg.memory_mib, cfg.cpus) |
| 220 |
|
- |
}), |
| 221 |
|
- |
}; |
| 222 |
|
- |
match hub.claim(&req) { |
| 223 |
|
- |
Ok(Some(claim)) => { |
| 224 |
|
- |
idle = Duration::from_secs(2); |
| 225 |
|
- |
let (mem, cpus) = (claim.sandbox.memory_mib, claim.sandbox.cpus); |
| 226 |
|
- |
{ |
| 227 |
|
- |
let mut u = used.lock().unwrap(); |
| 228 |
|
- |
u.jobs += 1; |
| 229 |
|
- |
u.memory_mib += mem; |
| 230 |
|
- |
u.cpus += cpus; |
| 231 |
|
- |
} |
| 232 |
|
- |
let (home, hub, cfg, used, key) = (home, &hub, &cfg, &used, &key); |
| 233 |
|
- |
scope.spawn(move || { |
| 234 |
|
- |
let id = claim.job_id; |
| 235 |
|
- |
eprintln!("job {id}: {} {} on {}", claim.repo, claim.job.name, claim.heads.first().map(|h| &h[..12]).unwrap_or("-")); |
| 236 |
|
- |
let finish = run_claim(home, hub, cfg, key, &claim).unwrap_or_else(|e| Finish { |
| 237 |
|
- |
exit_code: None, |
| 238 |
|
- |
duration_ms: 0, |
| 239 |
|
- |
receipt: None, |
| 240 |
|
- |
signed_receipt: None, |
| 241 |
|
- |
error: Some(format!("{e:#}")), |
| 242 |
|
- |
}); |
| 243 |
|
- |
eprintln!( |
| 244 |
|
- |
"job {id}: exit {:?}{}", |
| 245 |
|
- |
finish.exit_code, |
| 246 |
|
- |
finish.error.as_deref().map(|e| format!(" ({e})")).unwrap_or_default() |
| 247 |
|
- |
); |
| 248 |
|
- |
if let Err(e) = hub.finish(id, &finish) { |
| 249 |
|
- |
eprintln!("job {id}: reporting failed: {e:#}"); |
|
266 |
+ |
let pending = report_pending(home); |
|
267 |
+ |
std::thread::scope(|scope| { |
|
268 |
+ |
for (job, f) in pending { |
|
269 |
+ |
let hub = &hub; |
|
270 |
+ |
scope.spawn(move || { |
|
271 |
+ |
eprintln!("job {job}: reporting the result kept from before the restart"); |
|
272 |
+ |
report_finish(home, hub, job, &f); |
|
273 |
+ |
}); |
|
274 |
+ |
} |
|
275 |
+ |
loop { |
|
276 |
+ |
let room = budget.room(*used.lock().unwrap()); |
|
277 |
+ |
let Some((free_memory_mib, free_cpus)) = room else { |
|
278 |
+ |
std::thread::sleep(Duration::from_millis(500)); |
|
279 |
+ |
continue; |
|
280 |
+ |
}; |
|
281 |
+ |
let req = ClaimRequest { |
|
282 |
+ |
runner: name.clone(), |
|
283 |
+ |
runtimes: vec!["microvm".into()], |
|
284 |
+ |
free_memory_mib, |
|
285 |
+ |
free_cpus, |
|
286 |
+ |
info: Some(RunnerInfo { |
|
287 |
+ |
version: env!("OXY_VERSION_LONG").into(), |
|
288 |
+ |
..RunnerInfo::here(cfg.labels.clone(), budget.max_jobs as u32, cfg.memory_mib, cfg.cpus) |
|
289 |
+ |
}), |
|
290 |
+ |
}; |
|
291 |
+ |
match hub.claim(&req) { |
|
292 |
+ |
Ok(Some(claim)) => { |
|
293 |
+ |
idle = Duration::from_secs(2); |
|
294 |
+ |
let (mem, cpus) = (claim.sandbox.memory_mib, claim.sandbox.cpus); |
|
295 |
+ |
{ |
|
296 |
+ |
let mut u = used.lock().unwrap(); |
|
297 |
+ |
u.jobs += 1; |
|
298 |
+ |
u.memory_mib += mem; |
|
299 |
+ |
u.cpus += cpus; |
| 250 |
300 |
|
} |
| 251 |
|
- |
let mut u = used.lock().unwrap(); |
| 252 |
|
- |
u.jobs -= 1; |
| 253 |
|
- |
u.memory_mib -= mem; |
| 254 |
|
- |
u.cpus -= cpus; |
| 255 |
|
- |
}); |
| 256 |
|
- |
} |
| 257 |
|
- |
Ok(None) => std::thread::sleep(idle), |
| 258 |
|
- |
Err(e) => { |
| 259 |
|
- |
eprintln!("claim: {e:#}"); |
| 260 |
|
- |
std::thread::sleep(idle); |
| 261 |
|
- |
idle = (idle * 2).min(Duration::from_secs(60)); |
|
301 |
+ |
let (home, hub, cfg, used, key) = (home, &hub, &cfg, &used, &key); |
|
302 |
+ |
scope.spawn(move || { |
|
303 |
+ |
let id = claim.job_id; |
|
304 |
+ |
eprintln!( |
|
305 |
+ |
"job {id}: {} {} on {}", |
|
306 |
+ |
claim.repo, |
|
307 |
+ |
claim.job.name, |
|
308 |
+ |
claim.heads.first().map(|h| &h[..12]).unwrap_or("-") |
|
309 |
+ |
); |
|
310 |
+ |
let finish = run_claim(home, hub, cfg, key, &claim).unwrap_or_else(|e| Finish { |
|
311 |
+ |
exit_code: None, |
|
312 |
+ |
duration_ms: 0, |
|
313 |
+ |
receipt: None, |
|
314 |
+ |
signed_receipt: None, |
|
315 |
+ |
error: Some(format!("{e:#}")), |
|
316 |
+ |
}); |
|
317 |
+ |
eprintln!( |
|
318 |
+ |
"job {id}: exit {:?}{}", |
|
319 |
+ |
finish.exit_code, |
|
320 |
+ |
finish.error.as_deref().map(|e| format!(" ({e})")).unwrap_or_default() |
|
321 |
+ |
); |
|
322 |
+ |
report_finish(home, hub, id, &finish); |
|
323 |
+ |
let mut u = used.lock().unwrap(); |
|
324 |
+ |
u.jobs -= 1; |
|
325 |
+ |
u.memory_mib -= mem; |
|
326 |
+ |
u.cpus -= cpus; |
|
327 |
+ |
}); |
|
328 |
+ |
} |
|
329 |
+ |
Ok(None) => std::thread::sleep(idle), |
|
330 |
+ |
Err(e) => { |
|
331 |
+ |
eprintln!("claim: {e:#}"); |
|
332 |
+ |
std::thread::sleep(idle); |
|
333 |
+ |
idle = (idle * 2).min(Duration::from_secs(60)); |
|
334 |
+ |
} |
| 262 |
335 |
|
} |
| 263 |
336 |
|
} |
| 264 |
337 |
|
}) |
|
|
… |
313 unchanged lines |
| 578 |
651 |
|
|
| 579 |
652 |
|
fn hub_at(base: String) -> Hub { |
| 580 |
653 |
|
Hub { base, token: "t".into(), agent: ureq::AgentBuilder::new().timeout(Duration::from_secs(5)).build() } |
|
654 |
+ |
} |
|
655 |
+ |
|
|
656 |
+ |
/// oxygenhub#71: a job's result is offered until the hub takes it, |
|
657 |
+ |
/// through server errors, and a refusal ends the trying at once. |
|
658 |
+ |
#[test] |
|
659 |
+ |
fn a_result_is_reported_until_the_hub_takes_it() { |
|
660 |
+ |
let f = Finish { exit_code: Some(0), duration_ms: 1, receipt: None, signed_receipt: None, error: None }; |
|
661 |
+ |
let quick = Duration::from_millis(10); |
|
662 |
+ |
let (base, seen) = fake_hub(vec![502, 503, 500, 500, 204]); |
|
663 |
+ |
hub_at(base).finish_until_accepted(7, &f, Duration::from_secs(30), quick).unwrap(); |
|
664 |
+ |
assert_eq!(seen.load(std::sync::atomic::Ordering::SeqCst), 5, "kept trying through four server errors"); |
|
665 |
+ |
let (base, seen) = fake_hub(vec![409]); |
|
666 |
+ |
assert!(hub_at(base).finish_until_accepted(7, &f, Duration::from_secs(30), quick).is_err()); |
|
667 |
+ |
assert_eq!(seen.load(std::sync::atomic::Ordering::SeqCst), 1, "a refusal is final"); |
| 581 |
668 |
|
} |
| 582 |
669 |
|
|
| 583 |
670 |
|
#[test] |
|
|
… |
32 unchanged lines |