|
|
… |
101 unchanged lines |
| 102 |
102 |
|
Ok(()) |
| 103 |
103 |
|
} |
| 104 |
104 |
|
|
|
105 |
+ |
/// Hand back a job this runner holds as it leaves (oxygenflow#76): the |
|
106 |
+ |
/// hub queues it again ahead of new work for another runner. |
|
107 |
+ |
pub fn release(&self, job: i64, reason: &str) -> Result<()> { |
|
108 |
+ |
self.post_patiently(&format!("/flow/jobs/{job}/release"), &Decline { reason: reason.into() })?; |
|
109 |
+ |
Ok(()) |
|
110 |
+ |
} |
|
111 |
+ |
|
| 105 |
112 |
|
/// Say this runner is there while it claims nothing: its slots are full, |
| 106 |
113 |
|
/// or it is draining (oxygenhub#243). Once, no retries: the next comes |
| 107 |
114 |
|
/// soon. A hub without the endpoint (404) is not an error. |
|
|
… |
472 unchanged lines |
| 580 |
587 |
|
/// it holds are done and reported. It may be started again afterwards. |
| 581 |
588 |
|
pub fn serve_embedded(home: &Home) -> Result<()> { |
| 582 |
589 |
|
STOPS.store(0, std::sync::atomic::Ordering::SeqCst); |
|
590 |
+ |
*LEAVING.lock().unwrap_or_else(|p| p.into_inner()) = None; |
| 583 |
591 |
|
serve_until_drained(home) |
|
592 |
+ |
} |
|
593 |
+ |
|
|
594 |
+ |
/// Why the runner is leaving, once [`leave`] says so: the jobs it holds are |
|
595 |
+ |
/// stopped and handed back rather than finished (oxygenflow#76). |
|
596 |
+ |
static LEAVING: std::sync::Mutex<Option<String>> = std::sync::Mutex::new(None); |
|
597 |
+ |
|
|
598 |
+ |
fn leaving() -> Option<String> { |
|
599 |
+ |
LEAVING.lock().unwrap_or_else(|p| p.into_inner()).clone() |
|
600 |
+ |
} |
|
601 |
+ |
|
|
602 |
+ |
/// Ask a runner started with [`serve_embedded`] to leave now: claim nothing |
|
603 |
+ |
/// more, stop the jobs it holds and hand them back to the hub, which queues |
|
604 |
+ |
/// them ahead of new work for another runner (oxygenflow#76), then return. |
|
605 |
+ |
/// For a departure with no time to finish: a phone off the allowed network. |
|
606 |
+ |
pub fn leave(reason: &str) { |
|
607 |
+ |
*LEAVING.lock().unwrap_or_else(|p| p.into_inner()) = Some(reason.to_string()); |
|
608 |
+ |
stop(); |
| 584 |
609 |
|
} |
| 585 |
610 |
|
|
| 586 |
611 |
|
/// Ask a runner started with [`serve_embedded`] to drain: claim nothing |
|
|
… |
170 unchanged lines |
| 757 |
782 |
|
crate::metrics::RUNNING.fetch_sub(1, std::sync::atomic::Ordering::Relaxed); |
| 758 |
783 |
|
let cancelled = finish.error.as_deref() == Some(CANCELLED); |
| 759 |
784 |
|
crate::metrics::finished(finish.exit_code, finish.error.as_deref(), finish.duration_ms, cancelled); |
| 760 |
|
- |
if !cancelled { |
| 761 |
|
- |
report_finish(home, hub, id, &finish); |
|
785 |
+ |
match leaving() { |
|
786 |
+ |
// Stopped as the runner leaves: another runner takes it. |
|
787 |
+ |
Some(why) if cancelled => { |
|
788 |
+ |
eprintln!("job {id}: handed back: {why}"); |
|
789 |
+ |
if let Err(e) = hub.release(id, &why) { |
|
790 |
+ |
eprintln!("job {id}: handing back: {e:#}; the hub requeues it once this runner's lease lapses"); |
|
791 |
+ |
} |
|
792 |
+ |
} |
|
793 |
+ |
_ if !cancelled => report_finish(home, hub, id, &finish), |
|
794 |
+ |
_ => {} |
| 762 |
795 |
|
} |
| 763 |
796 |
|
held.lock().unwrap().remove(&id); |
| 764 |
797 |
|
let mut u = used.lock().unwrap(); |
|
|
… |
48 unchanged lines |
| 813 |
846 |
|
buf: Vec<Record>, |
| 814 |
847 |
|
last: Instant, |
| 815 |
848 |
|
gone: bool, |
|
849 |
+ |
/// Set when the runner leaves ([`LEAVING`]): the job stops, to be |
|
850 |
+ |
/// handed back. |
|
851 |
+ |
leaving: &'a std::sync::Mutex<Option<String>>, |
| 816 |
852 |
|
} |
| 817 |
853 |
|
|
| 818 |
854 |
|
/// The most log lines sent at once, as a busy job's sink flushes them. |
|
|
… |
11 unchanged lines |
| 830 |
866 |
|
} |
| 831 |
867 |
|
|
| 832 |
868 |
|
fn cancelled(&mut self) -> bool { |
|
869 |
+ |
if self.leaving.lock().unwrap_or_else(|p| p.into_inner()).is_some() { |
|
870 |
+ |
// Handed back as the runner leaves: what the job said so far first. |
|
871 |
+ |
self.flush(); |
|
872 |
+ |
return true; |
|
873 |
+ |
} |
| 833 |
874 |
|
if !self.gone && self.last.elapsed() >= HEARTBEAT { |
| 834 |
875 |
|
self.send(); |
| 835 |
876 |
|
} |
|
|
… |
99 unchanged lines |
| 935 |
976 |
|
|
| 936 |
977 |
|
fn run_claim(home: &Home, hub: &Hub, cfg: &RunnerConfig, key: &RunnerKey, claim: &Claim) -> Result<Finish> { |
| 937 |
978 |
|
let started = Instant::now(); |
| 938 |
|
- |
let mut sink = LogSink { hub, job: claim.job_id, buf: Vec::new(), last: Instant::now(), gone: false }; |
|
979 |
+ |
let mut sink = LogSink { hub, job: claim.job_id, buf: Vec::new(), last: Instant::now(), gone: false, leaving: &LEAVING }; |
| 939 |
980 |
|
let note = |
| 940 |
981 |
|
|sink: &mut LogSink, text: String| sink.push(Record { t: started.elapsed().as_millis() as u64, stream: "flow".into(), text }); |
| 941 |
982 |
|
let heads: Vec<ChangeHash> = claim.heads.iter().map(|h| ChangeHash::from_hex(h).context("bad head")).collect::<Result<_>>()?; |
|
|
… |
750 unchanged lines |
| 1692 |
1733 |
|
let (base, seen) = fake_hub(vec![401]); |
| 1693 |
1734 |
|
assert!(hub_at(base).post_with("/flow/claim", &serde_json::json!({}), &quick).is_err()); |
| 1694 |
1735 |
|
assert_eq!(seen.load(std::sync::atomic::Ordering::SeqCst), 1, "a refusal is final"); |
|
1736 |
+ |
} |
|
1737 |
+ |
|
|
1738 |
+ |
static NOT_LEAVING: std::sync::Mutex<Option<String>> = std::sync::Mutex::new(None); |
|
1739 |
+ |
|
|
1740 |
+ |
/// oxygenflow#76: once the runner leaves, a job's sink says it is |
|
1741 |
+ |
/// cancelled, having sent what the job said so far, and the job is |
|
1742 |
+ |
/// handed back to the hub. |
|
1743 |
+ |
#[test] |
|
1744 |
+ |
fn a_leaving_runner_stops_its_job_and_hands_it_back() { |
|
1745 |
+ |
use microvm::Sink; |
|
1746 |
+ |
let leaving = std::sync::Mutex::new(None); |
|
1747 |
+ |
let (base, seen) = fake_hub(vec![204, 204]); |
|
1748 |
+ |
let hub = hub_at(base); |
|
1749 |
+ |
let mut sink = LogSink { hub: &hub, job: 7, buf: Vec::new(), last: Instant::now(), gone: false, leaving: &leaving }; |
|
1750 |
+ |
sink.buf.push(Record { t: 1, stream: "out".into(), text: "step 1".into() }); |
|
1751 |
+ |
assert!(!sink.cancelled()); |
|
1752 |
+ |
*leaving.lock().unwrap() = Some("off Wi-Fi".into()); |
|
1753 |
+ |
assert!(sink.cancelled()); |
|
1754 |
+ |
assert!(sink.buf.is_empty(), "flushed before the job stops"); |
|
1755 |
+ |
assert_eq!(seen.load(std::sync::atomic::Ordering::SeqCst), 1); |
|
1756 |
+ |
hub.release(7, "off Wi-Fi").unwrap(); |
|
1757 |
+ |
assert_eq!(seen.load(std::sync::atomic::Ordering::SeqCst), 2); |
| 1695 |
1758 |
|
} |
| 1696 |
1759 |
|
|
| 1697 |
1760 |
|
/// oxygenflow#32: a quiet job asks the hub every few seconds whether |
|
|
… |
5 unchanged lines |
| 1703 |
1766 |
|
let quiet = Instant::now() - HEARTBEAT - Duration::from_secs(1); |
| 1704 |
1767 |
|
let (base, seen) = fake_hub(vec![204]); |
| 1705 |
1768 |
|
let hub = hub_at(base); |
| 1706 |
|
- |
let mut sink = LogSink { hub: &hub, job: 1, buf: Vec::new(), last: quiet, gone: false }; |
|
1769 |
+ |
let mut sink = LogSink { hub: &hub, job: 1, buf: Vec::new(), last: quiet, gone: false, leaving: &NOT_LEAVING }; |
| 1707 |
1770 |
|
assert!(!sink.cancelled()); |
| 1708 |
1771 |
|
assert!(!sink.cancelled(), "asked again only after the heartbeat"); |
| 1709 |
1772 |
|
assert_eq!(n(&seen), 1); |
| 1710 |
1773 |
|
|
| 1711 |
1774 |
|
let (base, seen) = fake_hub(vec![409]); |
| 1712 |
1775 |
|
let hub = hub_at(base); |
| 1713 |
|
- |
let mut sink = LogSink { hub: &hub, job: 1, buf: Vec::new(), last: quiet, gone: false }; |
|
1776 |
+ |
let mut sink = LogSink { hub: &hub, job: 1, buf: Vec::new(), last: quiet, gone: false, leaving: &NOT_LEAVING }; |
| 1714 |
1777 |
|
assert!(sink.cancelled()); |
| 1715 |
1778 |
|
sink.record(Record { t: 0, stream: "out".into(), text: "late".into() }); |
| 1716 |
1779 |
|
sink.flush(); |
|
|
… |
13 unchanged lines |
| 1730 |
1793 |
|
let hub = hub_at(base.clone()); |
| 1731 |
1794 |
|
|
| 1732 |
1795 |
|
// Log lines sent while it is away are kept, and go when it is back. |
| 1733 |
|
- |
let mut sink = LogSink { hub: &hub, job: 1, buf: Vec::new(), last: Instant::now(), gone: false }; |
|
1796 |
+ |
let mut sink = LogSink { hub: &hub, job: 1, buf: Vec::new(), last: Instant::now(), gone: false, leaving: &NOT_LEAVING }; |
| 1734 |
1797 |
|
for i in 0..450 { |
| 1735 |
1798 |
|
sink.buf.push(Record { t: i, stream: "out".into(), text: format!("line {i}") }); |
| 1736 |
1799 |
|
} |
|
|
… |
43 unchanged lines |
| 1780 |
1843 |
|
fn unsent_log_lines_are_bounded() { |
| 1781 |
1844 |
|
let port = std::net::TcpListener::bind("127.0.0.1:0").unwrap().local_addr().unwrap().port(); |
| 1782 |
1845 |
|
let hub = hub_at(format!("http://127.0.0.1:{port}")); |
| 1783 |
|
- |
let mut sink = LogSink { hub: &hub, job: 1, buf: Vec::new(), last: Instant::now(), gone: false }; |
|
1846 |
+ |
let mut sink = LogSink { hub: &hub, job: 1, buf: Vec::new(), last: Instant::now(), gone: false, leaving: &NOT_LEAVING }; |
| 1784 |
1847 |
|
sink.buf = (0..MAX_UNSENT as u64 + 10).map(|i| Record { t: i, stream: "out".into(), text: String::new() }).collect(); |
| 1785 |
1848 |
|
sink.send(); |
| 1786 |
1849 |
|
assert_eq!(sink.buf.len(), MAX_UNSENT + 1); |
|
|
… |
19 unchanged lines |