use std::time::Duration;
use argonauts_core::events::RunEventKind;
use argonauts_core::events::run::Party;
use argonauts_core::events::session::SessionEnd;
use argonauts_core::events::workflow::{RunNote, WorkflowEvent};
use argonauts_core::limits::{
NETWORK_BACK, NETWORK_DOWN, NetState, may_have_lost_the_network, served_on_this_machine,
};
use argonauts_core::names::ModelRef;
use argonauts_core::settings::endpoints::endpoint_settings;
use argonauts_core::{Note, NoteLevel};
use tokio::sync::watch;
use tokio_util::sync::CancellationToken;
use super::Host;
use crate::engine::commands;
use crate::engine::job_fallback;
use crate::engine::shared::Admission;
use crate::error::{EngineError, EngineResult};
use crate::state::sessions::SessionRecord;
impl Host {
pub(super) async fn wait_for_the_network(&self, worker: &ModelRef) -> EngineResult<()> {
self.follow_the_network();
if self.works_offline(worker) {
return Ok(());
}
let health = self.services().health.clone();
let mut changes = health.network_changes();
let mut admission = self.shared().admission.subscribe();
let halt = self.shared().halt.clone();
loop {
if !health.network().is_down() || *admission.borrow_and_update() != Admission::Open {
return Ok(());
}
tokio::select! {
changed = changes.changed() => if changed.is_err() {
return Ok(());
},
changed = admission.changed() => if changed.is_err() {
return Err(EngineError::Interrupted);
},
() = halt.cancelled() => return Err(EngineError::Interrupted),
}
}
}
pub(super) async fn lost_the_network(&self, worker: &ModelRef, end: &SessionEnd) -> bool {
may_have_lost_the_network(&end.reason)
&& !self.works_offline(worker)
&& self.services().health.check_network().await.is_down()
}
pub(super) async fn lost_the_network_in(&self, record: &SessionRecord) -> bool {
match &record.end {
Some(end) => self.lost_the_network(&record.started.worker, end).await,
None => false,
}
}
fn works_offline(&self, worker: &ModelRef) -> bool {
served_on_this_machine(&worker.agent, worker.model.as_deref())
|| endpoint_settings(self.settings(), &worker.agent).is_some_and(|ends| ends.local)
}
fn follow_the_network(&self) {
if !job_fallback::begin_following_the_network(self) {
return;
}
let host = self.clone();
tokio::spawn(async move { host.follow().await });
}
async fn follow(&self) {
let mut changes = self.services().health.network_changes();
let halt = self.shared().halt.clone();
let resumed_at = self
.read(|state| state.run.last_resumed.is_some())
.then(|| self.now());
loop {
let since = loop {
if let NetState::Down { since } = *changes.borrow_and_update() {
break resumed_at.map_or(since, |resumed| since.max(resumed));
}
if !changed(&mut changes, &halt).await {
return;
}
};
if !self.settled() {
self.say(NETWORK_DOWN);
}
let down_for = Duration::try_from(self.now() - since).unwrap_or_default();
let left = self.settings().network.hold().saturating_sub(down_for);
let held_too_long = tokio::time::sleep(left);
tokio::pin!(held_too_long);
let back = loop {
if !changes.borrow_and_update().is_down() {
break true;
}
tokio::select! {
changed = changes.changed() => if changed.is_err() {
return;
},
() = &mut held_too_long => break false,
() = halt.cancelled() => return,
}
};
if back {
if !self.settled() {
self.say(NETWORK_BACK);
}
continue;
}
self.pause_for_the_network().await;
job_fallback::end_following_the_network(self);
return;
}
}
fn settled(&self) -> bool {
self.read(|state| {
let run = &state.run;
run.finished.is_some()
|| run.paused
|| run.pause_requested.is_some()
|| run.stop_requested
})
}
fn say(&self, text: &str) {
let note = RunEventKind::Note(Note {
level: NoteLevel::Info,
text: text.to_owned(),
});
let in_the_views = RunEventKind::Workflow(WorkflowEvent::Note(RunNote {
text: text.to_owned(),
workflow: None,
left: false,
}));
for kind in [note, in_the_views] {
if let Err(err) = self.record(None, None, kind) {
tracing::warn!(%err, "couldn't say the network changed");
}
}
}
async fn pause_for_the_network(&self) {
if self.settled() {
return;
}
let minutes = self.settings().network.hold_minutes;
let held = match minutes {
1 => "a minute".to_owned(),
minutes => format!("{minutes} minutes"),
};
let note = RunEventKind::Note(Note {
level: NoteLevel::Warn,
text: format!(
"The network has been down for {held}, so the run pauses here with its work \
kept. Resume it once the internet is back."
),
});
let paused = match self.record(None, None, note) {
Ok(_) => match commands::begin_pause_by(self, true, Party::Network) {
Ok(true) => commands::finish_pause(self).await,
Ok(false) => Ok(()),
Err(err) => Err(err),
},
Err(err) => Err(err),
};
if let Err(err) = paused {
tracing::error!(%err, "the network stayed down, and pausing the run failed");
}
}
}
async fn changed(changes: &mut watch::Receiver<NetState>, halt: &CancellationToken) -> bool {
tokio::select! {
changed = changes.changed() => changed.is_ok(),
() = halt.cancelled() => false,
}
}