|
|
… |
71 unchanged lines |
| 72 |
72 |
|
|
| 73 |
73 |
|
fn post_with(&self, path: &str, body: &impl serde::Serialize, delays: &[Duration]) -> Result<Option<ureq::Response>> { |
| 74 |
74 |
|
let url = format!("{}/api/v1{path}", self.base); |
| 75 |
|
- |
let mut delays = delays.iter(); |
| 76 |
|
- |
loop { |
| 77 |
|
- |
match self.agent.post(&url).set("Authorization", &format!("Bearer {}", self.token)).send_json(body) { |
| 78 |
|
- |
Ok(r) if r.status() == 204 => return Ok(None), |
| 79 |
|
- |
Ok(r) => return Ok(Some(r)), |
| 80 |
|
- |
Err(ureq::Error::Status(code, r)) => { |
| 81 |
|
- |
let text = r.into_string().unwrap_or_default(); |
| 82 |
|
- |
match delays.next() { |
| 83 |
|
- |
Some(d) if code >= 500 => std::thread::sleep(*d), |
| 84 |
|
- |
_ if code < 500 => return Err(anyhow::Error::new(Refused { code, text }).context(url)), |
| 85 |
|
- |
_ => bail!("{url}: HTTP {code}: {text}"), |
| 86 |
|
- |
} |
| 87 |
|
- |
} |
| 88 |
|
- |
Err(e) => bail!("{url}: {e}"), |
| 89 |
|
- |
} |
| 90 |
|
- |
} |
|
75 |
+ |
let r = retrying(&url, delays, false, || { |
|
76 |
+ |
self.agent.post(&url).set("Authorization", &format!("Bearer {}", self.token)).send_json(body).map_err(Box::new) |
|
77 |
+ |
})?; |
|
78 |
+ |
Ok(if r.status() == 204 { None } else { Some(r) }) |
|
79 |
+ |
} |
|
80 |
+ |
|
|
81 |
+ |
/// POST what a job leaves behind (oxygenflow#49): through a hub that is |
|
82 |
+ |
/// away for a few minutes, as while a deploy restarts it, so a job's |
|
83 |
+ |
/// declines and uploads outlast it. |
|
84 |
+ |
fn post_patiently(&self, path: &str, body: &impl serde::Serialize) -> Result<Option<ureq::Response>> { |
|
85 |
+ |
let url = format!("{}/api/v1{path}", self.base); |
|
86 |
+ |
let r = retrying(&url, &PATIENT_DELAYS, true, || { |
|
87 |
+ |
self.agent.post(&url).set("Authorization", &format!("Bearer {}", self.token)).send_json(body).map_err(Box::new) |
|
88 |
+ |
})?; |
|
89 |
+ |
Ok(if r.status() == 204 { None } else { Some(r) }) |
| 91 |
90 |
|
} |
| 92 |
91 |
|
|
| 93 |
92 |
|
pub fn claim(&self, req: &ClaimRequest) -> Result<Option<Claim>> { |
|
|
… |
5 unchanged lines |
| 99 |
98 |
|
|
| 100 |
99 |
|
/// Hand job `job` back, saying why this runner will not run it. |
| 101 |
100 |
|
pub fn decline(&self, job: i64, reason: &str) -> Result<()> { |
| 102 |
|
- |
self.post(&format!("/flow/jobs/{job}/decline"), &Decline { reason: reason.into() })?; |
|
101 |
+ |
self.post_patiently(&format!("/flow/jobs/{job}/decline"), &Decline { reason: reason.into() })?; |
| 103 |
102 |
|
Ok(()) |
| 104 |
103 |
|
} |
| 105 |
104 |
|
|
|
|
… |
6 unchanged lines |
| 112 |
111 |
|
/// a blob of the job's repository and answers with its ObjectRef. |
| 113 |
112 |
|
pub fn artifact(&self, job: i64, name: &str, bytes: &[u8]) -> Result<String> { |
| 114 |
113 |
|
let url = format!("{}/api/v1/flow/jobs/{job}/artifacts/{name}", self.base); |
| 115 |
|
- |
let mut delays = RETRY_DELAYS.iter(); |
| 116 |
|
- |
loop { |
| 117 |
|
- |
let req = self |
| 118 |
|
- |
.agent |
|
114 |
+ |
let r = retrying(&url, &PATIENT_DELAYS, true, || { |
|
115 |
+ |
self.agent |
| 119 |
116 |
|
.post(&url) |
| 120 |
117 |
|
.timeout(Duration::from_secs(600)) |
| 121 |
118 |
|
.set("Authorization", &format!("Bearer {}", self.token)) |
| 122 |
|
- |
.set("Content-Type", "application/octet-stream"); |
| 123 |
|
- |
match req.send_bytes(bytes) { |
| 124 |
|
- |
Ok(r) => { |
| 125 |
|
- |
let v: serde_json::Value = r.into_json()?; |
| 126 |
|
- |
return v["blob"].as_str().map(String::from).context("the hub named no blob"); |
| 127 |
|
- |
} |
| 128 |
|
- |
Err(ureq::Error::Status(code, r)) => { |
| 129 |
|
- |
let text = r.into_string().unwrap_or_default(); |
| 130 |
|
- |
match delays.next() { |
| 131 |
|
- |
Some(d) if code >= 500 => std::thread::sleep(*d), |
| 132 |
|
- |
_ => bail!("{url}: HTTP {code}: {text}"), |
| 133 |
|
- |
} |
| 134 |
|
- |
} |
| 135 |
|
- |
Err(e) => bail!("{url}: {e}"), |
| 136 |
|
- |
} |
| 137 |
|
- |
} |
|
119 |
+ |
.set("Content-Type", "application/octet-stream") |
|
120 |
+ |
.send_bytes(bytes) |
|
121 |
+ |
.map_err(Box::new) |
|
122 |
+ |
})?; |
|
123 |
+ |
let v: serde_json::Value = r.into_json()?; |
|
124 |
+ |
v["blob"].as_str().map(String::from).context("the hub named no blob") |
| 138 |
125 |
|
} |
| 139 |
126 |
|
|
| 140 |
127 |
|
/// Upload file `path` of report `name` of job `job` (oxygenhub#127); |
|
|
… |
1 unchanged lines |
| 142 |
129 |
|
/// report's manifest follows through [`Hub::artifact`]. |
| 143 |
130 |
|
pub fn report_file(&self, job: i64, name: &str, path: &str, bytes: &[u8]) -> Result<String> { |
| 144 |
131 |
|
let url = format!("{}/api/v1/flow/jobs/{job}/reports/{name}/files", self.base); |
| 145 |
|
- |
let mut delays = RETRY_DELAYS.iter(); |
| 146 |
|
- |
loop { |
| 147 |
|
- |
let req = self |
| 148 |
|
- |
.agent |
|
132 |
+ |
let r = retrying(&url, &PATIENT_DELAYS, true, || { |
|
133 |
+ |
self.agent |
| 149 |
134 |
|
.post(&url) |
| 150 |
135 |
|
.timeout(Duration::from_secs(600)) |
| 151 |
136 |
|
.set("Authorization", &format!("Bearer {}", self.token)) |
| 152 |
137 |
|
.set("Content-Type", "application/octet-stream") |
| 153 |
|
- |
.set("X-Flow-Report-Path", path); |
| 154 |
|
- |
match req.send_bytes(bytes) { |
| 155 |
|
- |
Ok(r) => { |
| 156 |
|
- |
let v: serde_json::Value = r.into_json()?; |
| 157 |
|
- |
return v["blob"].as_str().map(String::from).context("the hub named no blob"); |
| 158 |
|
- |
} |
| 159 |
|
- |
Err(ureq::Error::Status(code, r)) => { |
| 160 |
|
- |
let text = r.into_string().unwrap_or_default(); |
| 161 |
|
- |
match delays.next() { |
| 162 |
|
- |
Some(d) if code >= 500 => std::thread::sleep(*d), |
| 163 |
|
- |
_ => bail!("{url}: HTTP {code}: {text}"), |
| 164 |
|
- |
} |
| 165 |
|
- |
} |
| 166 |
|
- |
Err(e) => bail!("{url}: {e}"), |
| 167 |
|
- |
} |
| 168 |
|
- |
} |
|
138 |
+ |
.set("X-Flow-Report-Path", path) |
|
139 |
+ |
.send_bytes(bytes) |
|
140 |
+ |
.map_err(Box::new) |
|
141 |
+ |
})?; |
|
142 |
+ |
let v: serde_json::Value = r.into_json()?; |
|
143 |
+ |
v["blob"].as_str().map(String::from).context("the hub named no blob") |
| 169 |
144 |
|
} |
| 170 |
145 |
|
|
| 171 |
146 |
|
/// Report a job's end until the hub has it (oxygenhub#71): while the hub |
|
|
… |
110 unchanged lines |
| 282 |
257 |
|
|
| 283 |
258 |
|
/// Waits before trying a hub request again after a server error. |
| 284 |
259 |
|
const RETRY_DELAYS: [Duration; 3] = [Duration::from_secs(1), Duration::from_secs(2), Duration::from_secs(4)]; |
|
260 |
+ |
|
|
261 |
+ |
/// Waits for what a job leaves behind (oxygenflow#49): three minutes in |
|
262 |
+ |
/// all, longer than a hub takes to restart. |
|
263 |
+ |
const PATIENT_DELAYS: [Duration; 15] = { |
|
264 |
+ |
let mut d = [Duration::from_secs(15); 15]; |
|
265 |
+ |
d[0] = Duration::from_secs(1); |
|
266 |
+ |
d[1] = Duration::from_secs(2); |
|
267 |
+ |
d[2] = Duration::from_secs(4); |
|
268 |
+ |
d[3] = Duration::from_secs(8); |
|
269 |
+ |
d |
|
270 |
+ |
}; |
|
271 |
+ |
|
|
272 |
+ |
/// Send a request until the hub answers, waiting `delays` in turn after a |
|
273 |
+ |
/// server error (5xx: nothing was committed) and, when `connection`, after |
|
274 |
+ |
/// the hub could not be reached at all. A refusal (4xx) is final, as a |
|
275 |
+ |
/// [`Refused`] the caller can look at. |
|
276 |
+ |
fn retrying( |
|
277 |
+ |
url: &str, |
|
278 |
+ |
delays: &[Duration], |
|
279 |
+ |
connection: bool, |
|
280 |
+ |
mut send: impl FnMut() -> Result<ureq::Response, Box<ureq::Error>>, |
|
281 |
+ |
) -> Result<ureq::Response> { |
|
282 |
+ |
let mut delays = delays.iter(); |
|
283 |
+ |
loop { |
|
284 |
+ |
let why = match send().map_err(|e| *e) { |
|
285 |
+ |
Ok(r) => return Ok(r), |
|
286 |
+ |
Err(ureq::Error::Status(code, r)) if code < 500 => { |
|
287 |
+ |
let text = r.into_string().unwrap_or_default(); |
|
288 |
+ |
return Err(anyhow::Error::new(Refused { code, text }).context(url.to_string())); |
|
289 |
+ |
} |
|
290 |
+ |
Err(ureq::Error::Status(code, r)) => format!("HTTP {code}: {}", r.into_string().unwrap_or_default()), |
|
291 |
+ |
Err(e) if connection => e.to_string(), |
|
292 |
+ |
Err(e) => bail!("{url}: {e}"), |
|
293 |
+ |
}; |
|
294 |
+ |
match delays.next() { |
|
295 |
+ |
Some(d) => std::thread::sleep(*d), |
|
296 |
+ |
None => bail!("{url}: {why}"), |
|
297 |
+ |
} |
|
298 |
+ |
} |
|
299 |
+ |
} |
| 285 |
300 |
|
|
| 286 |
301 |
|
/// What the runner may run at once (flow.toml `[runner]`): at most |
| 287 |
302 |
|
/// `max_jobs` jobs, whose sandboxes together fit `memory_mib` and `cpus`. |
|
|
… |
175 unchanged lines |
| 463 |
478 |
|
let hub = Hub::new(&cfg)?; |
| 464 |
479 |
|
let name = cfg.name.clone().unwrap_or_else(|| "flow".into()); |
| 465 |
480 |
|
let budget = Budget::of(&cfg); |
|
481 |
+ |
drain_on_stop(); |
| 466 |
482 |
|
let key = home.runner_key(&name)?; |
| 467 |
483 |
|
// The swarm (docs/jetstream.md §8.1): a runner that cannot serve its |
| 468 |
484 |
|
// peers still runs jobs, fetching images from the hub alone. |
|
|
… |
34 unchanged lines |
| 503 |
519 |
|
}; |
| 504 |
520 |
|
let mut speed = measure(); |
| 505 |
521 |
|
let mut benched = Instant::now(); |
|
522 |
+ |
let mut said_draining = false; |
| 506 |
523 |
|
loop { |
|
524 |
+ |
// Draining (oxygenflow#50): told to stop, claim nothing more and |
|
525 |
+ |
// stop once every job held is done and reported. |
|
526 |
+ |
if stopping() { |
|
527 |
+ |
let (jobs, holding) = (used.lock().unwrap().jobs, held.lock().unwrap().len()); |
|
528 |
+ |
if jobs == 0 && holding == 0 { |
|
529 |
+ |
eprintln!("flow runner {name}: drained, stopping"); |
|
530 |
+ |
break; |
|
531 |
+ |
} |
|
532 |
+ |
if !said_draining { |
|
533 |
+ |
eprintln!("flow runner {name}: draining: claiming nothing more, stopping when the {jobs} job(s) held are done (stop again to stop now)"); |
|
534 |
+ |
said_draining = true; |
|
535 |
+ |
} |
|
536 |
+ |
std::thread::sleep(Duration::from_millis(500)); |
|
537 |
+ |
continue; |
|
538 |
+ |
} |
| 507 |
539 |
|
if used.lock().unwrap().jobs == 0 && benched.elapsed() >= Duration::from_secs(15 * 60) { |
| 508 |
540 |
|
speed = measure(); |
| 509 |
541 |
|
benched = Instant::now(); |
|
|
… |
85 unchanged lines |
| 595 |
627 |
|
} |
| 596 |
628 |
|
} |
| 597 |
629 |
|
} |
|
630 |
+ |
Ok(()) |
| 598 |
631 |
|
}) |
|
632 |
+ |
} |
|
633 |
+ |
|
|
634 |
+ |
/// How many times the runner was told to stop (SIGTERM or SIGINT). |
|
635 |
+ |
static STOPS: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); |
|
636 |
+ |
|
|
637 |
+ |
extern "C" fn on_stop(_: libc::c_int) { |
|
638 |
+ |
// A second stop means now: only async-signal-safe calls here. |
|
639 |
+ |
if STOPS.fetch_add(1, std::sync::atomic::Ordering::SeqCst) >= 1 { |
|
640 |
+ |
unsafe { libc::_exit(143) }; |
|
641 |
+ |
} |
|
642 |
+ |
} |
|
643 |
+ |
|
|
644 |
+ |
/// Drain instead of dying on the first SIGTERM or SIGINT (oxygenflow#50), |
|
645 |
+ |
/// so restarting a runner never kills the job it runs: the unit's |
|
646 |
+ |
/// `KillMode=mixed` sends the signal to the runner alone, not its microVMs. |
|
647 |
+ |
fn drain_on_stop() { |
|
648 |
+ |
let handler = on_stop as extern "C" fn(libc::c_int) as libc::sighandler_t; |
|
649 |
+ |
unsafe { |
|
650 |
+ |
libc::signal(libc::SIGTERM, handler); |
|
651 |
+ |
libc::signal(libc::SIGINT, handler); |
|
652 |
+ |
} |
| 599 |
653 |
|
} |
| 600 |
654 |
|
|
|
655 |
+ |
fn stopping() -> bool { |
|
656 |
+ |
STOPS.load(std::sync::atomic::Ordering::SeqCst) > 0 |
|
657 |
+ |
} |
|
658 |
+ |
|
| 601 |
659 |
|
/// Batches records to the hub every second (or every 200 lines). A quiet |
| 602 |
660 |
|
/// job's sink still asks the hub every few seconds whether the job runs, |
| 603 |
661 |
|
/// with an empty batch; once the hub answers 409 the job is not wanted. |
|
|
… |
4 unchanged lines |
| 608 |
666 |
|
last: Instant, |
| 609 |
667 |
|
gone: bool, |
| 610 |
668 |
|
} |
|
669 |
+ |
|
|
670 |
+ |
/// The most log lines sent at once, as a busy job's sink flushes them. |
|
671 |
+ |
const BATCH: usize = 200; |
| 611 |
672 |
|
|
|
673 |
+ |
/// The most log lines kept while the hub cannot be reached. |
|
674 |
+ |
const MAX_UNSENT: usize = 50_000; |
|
675 |
+ |
|
| 612 |
676 |
|
/// How often a quiet job asks whether it was cancelled. |
| 613 |
677 |
|
const HEARTBEAT: Duration = Duration::from_secs(5); |
| 614 |
678 |
|
|
|
|
… |
23 unchanged lines |
| 638 |
702 |
|
r.text.truncate(cut); |
| 639 |
703 |
|
} |
| 640 |
704 |
|
self.buf.push(r); |
| 641 |
|
- |
if self.buf.len() >= 200 || self.last.elapsed() >= Duration::from_secs(1) { |
|
705 |
+ |
if self.buf.len() >= BATCH || self.last.elapsed() >= Duration::from_secs(1) { |
| 642 |
706 |
|
self.flush(); |
| 643 |
707 |
|
} |
| 644 |
708 |
|
} |
|
|
… |
5 unchanged lines |
| 650 |
714 |
|
self.last = Instant::now(); |
| 651 |
715 |
|
} |
| 652 |
716 |
|
|
| 653 |
|
- |
/// Send what is buffered, even nothing. |
|
717 |
+ |
/// Send what is buffered, even nothing, in batches of at most |
|
718 |
+ |
/// `BATCH` lines. |
| 654 |
719 |
|
fn send(&mut self) { |
| 655 |
|
- |
if self.gone { |
| 656 |
|
- |
return; |
| 657 |
|
- |
} |
| 658 |
|
- |
match self.hub.log(self.job, std::mem::take(&mut self.buf)) { |
| 659 |
|
- |
Ok(()) => {} |
| 660 |
|
- |
Err(e) if job_gone(&e) => self.gone = true, |
| 661 |
|
- |
Err(e) => eprintln!("job {}: log: {e:#}", self.job), |
|
720 |
+ |
loop { |
|
721 |
+ |
if self.gone { |
|
722 |
+ |
return; |
|
723 |
+ |
} |
|
724 |
+ |
let n = self.buf.len().min(BATCH); |
|
725 |
+ |
let batch: Vec<Record> = self.buf.drain(..n).collect(); |
|
726 |
+ |
match self.hub.log(self.job, batch.clone()) { |
|
727 |
+ |
Ok(()) => {} |
|
728 |
+ |
Err(e) if job_gone(&e) => self.gone = true, |
|
729 |
+ |
Err(e) => { |
|
730 |
+ |
// The hub is away (a restart, say): keep the lines for |
|
731 |
+ |
// the next batch, up to a bound, so the log loses none |
|
732 |
+ |
// (oxygenflow#49). |
|
733 |
+ |
eprintln!("job {}: log: {e:#}; kept for the next batch", self.job); |
|
734 |
+ |
let mut kept = batch; |
|
735 |
+ |
kept.append(&mut self.buf); |
|
736 |
+ |
let over = kept.len().saturating_sub(MAX_UNSENT); |
|
737 |
+ |
if over > 0 { |
|
738 |
+ |
kept.drain(..over); |
|
739 |
+ |
kept.insert( |
|
740 |
+ |
0, |
|
741 |
+ |
Record { |
|
742 |
+ |
t: 0, |
|
743 |
+ |
stream: "flow".into(), |
|
744 |
+ |
text: format!("({over} log lines lost while the hub could not be reached)"), |
|
745 |
+ |
}, |
|
746 |
+ |
); |
|
747 |
+ |
} |
|
748 |
+ |
self.buf = kept; |
|
749 |
+ |
break; |
|
750 |
+ |
} |
|
751 |
+ |
} |
|
752 |
+ |
if self.buf.is_empty() { |
|
753 |
+ |
break; |
|
754 |
+ |
} |
| 662 |
755 |
|
} |
| 663 |
756 |
|
self.last = Instant::now(); |
| 664 |
757 |
|
} |
|
|
… |
407 unchanged lines |
| 1072 |
1165 |
|
|
| 1073 |
1166 |
|
/// A hub that answers each request with the next of `codes`, counting them. |
| 1074 |
1167 |
|
fn fake_hub(codes: Vec<u16>) -> (String, std::sync::Arc<std::sync::atomic::AtomicUsize>) { |
|
1168 |
+ |
fake_hub_on(std::net::TcpListener::bind("127.0.0.1:0").unwrap(), codes) |
|
1169 |
+ |
} |
|
1170 |
+ |
|
|
1171 |
+ |
/// A hub at `listener` answering each request with the next of `codes`. |
|
1172 |
+ |
fn fake_hub_on(listener: std::net::TcpListener, codes: Vec<u16>) -> (String, std::sync::Arc<std::sync::atomic::AtomicUsize>) { |
| 1075 |
1173 |
|
use std::io::{Read, Write}; |
| 1076 |
|
- |
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); |
| 1077 |
1174 |
|
let addr = listener.local_addr().unwrap(); |
| 1078 |
1175 |
|
let seen = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)); |
| 1079 |
1176 |
|
let count = seen.clone(); |
|
|
… |
139 unchanged lines |
| 1219 |
1316 |
|
sink.flush(); |
| 1220 |
1317 |
|
assert!(sink.cancelled()); |
| 1221 |
1318 |
|
assert_eq!(n(&seen), 1, "nothing is sent for a cancelled job"); |
|
1319 |
+ |
} |
|
1320 |
+ |
|
|
1321 |
+ |
/// oxygenflow#49: a hub that is away for a while (a deploy restarting |
|
1322 |
+ |
/// it) costs a job's uploads a wait, not the upload, and costs its log |
|
1323 |
+ |
/// no lines. |
|
1324 |
+ |
#[test] |
|
1325 |
+ |
fn a_job_outlasts_a_hub_restart() { |
|
1326 |
+ |
let n = |seen: &std::sync::Arc<std::sync::atomic::AtomicUsize>| seen.load(std::sync::atomic::Ordering::SeqCst); |
|
1327 |
+ |
// A port nobody listens on yet: the hub is restarting. |
|
1328 |
+ |
let port = std::net::TcpListener::bind("127.0.0.1:0").unwrap().local_addr().unwrap().port(); |
|
1329 |
+ |
let base = format!("http://127.0.0.1:{port}"); |
|
1330 |
+ |
let hub = hub_at(base.clone()); |
|
1331 |
+ |
|
|
1332 |
+ |
// Log lines sent while it is away are kept, and go when it is back. |
|
1333 |
+ |
let mut sink = LogSink { hub: &hub, job: 1, buf: Vec::new(), last: Instant::now(), gone: false }; |
|
1334 |
+ |
for i in 0..450 { |
|
1335 |
+ |
sink.buf.push(Record { t: i, stream: "out".into(), text: format!("line {i}") }); |
|
1336 |
+ |
} |
|
1337 |
+ |
sink.send(); |
|
1338 |
+ |
assert_eq!(sink.buf.len(), 450, "nothing lost while the hub is away"); |
|
1339 |
+ |
let (_, seen) = fake_hub_on(std::net::TcpListener::bind(("127.0.0.1", port)).unwrap(), vec![204, 204, 204]); |
|
1340 |
+ |
sink.send(); |
|
1341 |
+ |
assert!(sink.buf.is_empty() && !sink.gone); |
|
1342 |
+ |
assert_eq!(n(&seen), 3, "in batches of 200: 200, 200 and 50"); |
|
1343 |
+ |
|
|
1344 |
+ |
// An upload waits for the hub to come back, through refused |
|
1345 |
+ |
// connections and server errors alike. |
|
1346 |
+ |
let port = std::net::TcpListener::bind("127.0.0.1:0").unwrap().local_addr().unwrap().port(); |
|
1347 |
+ |
let url = format!("http://127.0.0.1:{port}/x"); |
|
1348 |
+ |
let agent = ureq::AgentBuilder::new().timeout(Duration::from_secs(5)).build(); |
|
1349 |
+ |
let later = std::thread::spawn(move || { |
|
1350 |
+ |
std::thread::sleep(Duration::from_millis(150)); |
|
1351 |
+ |
fake_hub_on(std::net::TcpListener::bind(("127.0.0.1", port)).unwrap(), vec![502, 204]) |
|
1352 |
+ |
}); |
|
1353 |
+ |
let quick = [Duration::from_millis(50); 20]; |
|
1354 |
+ |
let r = retrying(&url, &quick, true, || agent.post(&url).send_string("{}").map_err(Box::new)).unwrap(); |
|
1355 |
+ |
assert_eq!(r.status(), 204); |
|
1356 |
+ |
assert_eq!(n(&later.join().unwrap().1), 2, "a 502 from the proxy, then the hub"); |
|
1357 |
+ |
// Without `connection`, a hub that cannot be reached ends it at once. |
|
1358 |
+ |
let port = std::net::TcpListener::bind("127.0.0.1:0").unwrap().local_addr().unwrap().port(); |
|
1359 |
+ |
let url = format!("http://127.0.0.1:{port}/x"); |
|
1360 |
+ |
assert!(retrying(&url, &quick, false, || agent.post(&url).send_string("{}").map_err(Box::new)).is_err()); |
|
1361 |
+ |
assert!(PATIENT_DELAYS.iter().sum::<Duration>() >= Duration::from_secs(180), "longer than a hub takes to restart"); |
|
1362 |
+ |
} |
|
1363 |
+ |
|
|
1364 |
+ |
/// oxygenflow#50: the first stop drains (the second, not called here, |
|
1365 |
+ |
/// exits at once). |
|
1366 |
+ |
#[test] |
|
1367 |
+ |
fn a_stop_drains_the_runner() { |
|
1368 |
+ |
assert!(!stopping()); |
|
1369 |
+ |
on_stop(libc::SIGTERM); |
|
1370 |
+ |
assert!(stopping(), "told to stop, it claims nothing more"); |
|
1371 |
+ |
} |
|
1372 |
+ |
|
|
1373 |
+ |
/// Lines kept for a hub that stays away are bounded, saying how many went. |
|
1374 |
+ |
#[test] |
|
1375 |
+ |
fn unsent_log_lines_are_bounded() { |
|
1376 |
+ |
let port = std::net::TcpListener::bind("127.0.0.1:0").unwrap().local_addr().unwrap().port(); |
|
1377 |
+ |
let hub = hub_at(format!("http://127.0.0.1:{port}")); |
|
1378 |
+ |
let mut sink = LogSink { hub: &hub, job: 1, buf: Vec::new(), last: Instant::now(), gone: false }; |
|
1379 |
+ |
sink.buf = (0..MAX_UNSENT as u64 + 10).map(|i| Record { t: i, stream: "out".into(), text: String::new() }).collect(); |
|
1380 |
+ |
sink.send(); |
|
1381 |
+ |
assert_eq!(sink.buf.len(), MAX_UNSENT + 1); |
|
1382 |
+ |
assert_eq!(sink.buf[0].text, "(10 log lines lost while the hub could not be reached)"); |
|
1383 |
+ |
assert_eq!(sink.buf[1].t, 10, "the oldest go first"); |
| 1222 |
1384 |
|
} |
| 1223 |
1385 |
|
|
| 1224 |
1386 |
|
#[test] |
|
|
… |
14 unchanged lines |