use std::fs;
use std::future::Future;
use std::io;
use std::path::Path;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use secrecy::SecretString;
use tokio::sync::broadcast::Receiver;
use tokio::sync::broadcast::error::RecvError;
use crate::events::Event;
use crate::exec::{ExecError, Executor, LocalExecutor, SshExecutor};
use crate::runs::{Outcome, RunCtx, RunError, RunRecord, RunState, Runs, watch};
use crate::train::{Pace, Trainer};
use super::provision::note_strays;
use super::{
CLIENT_KEY, DeleteReason, DeletedBy, Pod, PodCtx, PodError, PodId, PodKeys, PodPlan, PodRecord,
PodState, PodStatus, Provisioned, RemoteStatus, RunpodTarget, SSH_DIR, SshEndpoint, alias,
provision, remove, sweep, write_config,
};
pub const DEADLINE_MARGIN: Duration = Duration::from_secs(5 * 60);
pub const RETRIEVED_MARKER: &str = ".pod/retrieved";
pub const MAX_HOURS_REACHED: &str = "max_hours reached: the pod was deleted before the job ended";
pub const LEASE_FILE: &str = ".pod/lease";
pub const LEASE_TTL: Duration = Duration::from_secs(15 * 60);
const LEASE_RENEW: Duration = Duration::from_secs(5 * 60);
const STALL: Duration = Duration::from_secs(30 * 60);
const LEASE_TICK: Duration = Duration::from_secs(60);
const GONE_LOOKS: u32 = 3;
pub async fn start_pod(
ctx: &PodCtx<'_>,
target: &RunpodTarget,
mut run: RunRecord,
keep: bool,
) -> Result<(PodRecord, Provisioned), PodError> {
let result = provision_run(ctx, target, &run, keep).await;
if let Err(error) = &result {
run.state = RunState::Failed;
run.message = Some(run_message(error));
if let Err(save_error) = ctx.runs.save(&run) {
tracing::warn!("cannot record run {} as failed: {save_error}", run.id);
}
if no_pod_left(ctx.runs, &run.id) {
forget_client_key(ctx.runs, &run.id);
}
}
result
}
fn no_pod_left(runs: &Runs, run_id: &str) -> bool {
match PodRecord::load(runs, run_id) {
Ok(Some(record)) => {
record.stray_pods.is_empty()
&& (record.pod_id.is_none() || record.state == PodState::Deleted)
},
Ok(None) => true,
Err(error) => {
tracing::warn!("cannot read the pod record of run {run_id}: {error}");
false
},
}
}
fn run_message(error: &PodError) -> String {
match error {
PodError::Api(api) => match api.status() {
Some(status) => format!("Runpod answered {status}"),
None => error.to_string(),
},
_ => error.to_string(),
}
}
async fn provision_run(
ctx: &PodCtx<'_>,
target: &RunpodTarget,
run: &RunRecord,
keep: bool,
) -> Result<(PodRecord, Provisioned), PodError> {
let ssh_dir = ctx.runs.run_dir(&run.id)?.join(SSH_DIR);
let keys = PodKeys::generate(&ssh_dir, &alias(&run.id))?;
let mut record = PodRecord::new(&run.id, keep, target.gpu_count, &keys.host_public);
record.save(ctx.runs)?;
let plan = PodPlan {
run_id: &run.id,
target,
keys: &keys,
ssh_dir: &ssh_dir,
workdir: target.workdir(),
api_url: ctx.client.base_url(),
};
let provisioned = provision(ctx, &plan, &mut record).await?;
Ok((record, provisioned))
}
pub fn job_started(runs: &Runs, pod: &mut PodRecord) -> Result<(), PodError> {
pod.state = PodState::Running;
pod.save(runs)?;
Ok(())
}
pub async fn follow<E: Executor, T: Trainer>(
ctx: &PodCtx<'_>,
run_ctx: &RunCtx<'_, E>,
trainer: &T,
record: RunRecord,
pod: &mut PodRecord,
) -> Result<Outcome, PodError> {
let id = record.id.clone();
let watched = watch_on_pod(run_ctx, trainer, record, pod).await;
settle_watch(ctx, pod, &id, watched).await
}
#[derive(Debug)]
pub enum Watched {
Ended(Box<Result<Outcome, RunError>>),
DeadlinePassed,
}
pub async fn watch_on_pod<E: Executor, T: Trainer>(
run_ctx: &RunCtx<'_, E>,
trainer: &T,
record: RunRecord,
pod: &PodRecord,
) -> Watched {
match until_deadline(pod) {
Some(wait) => {
let events = run_ctx.bus.subscribe();
tokio::select! {
outcome = watch(run_ctx, trainer, record) => Watched::Ended(Box::new(outcome)),
() = tokio::time::sleep(wait) => Watched::DeadlinePassed,
() = warn_overrun(events, pod) => Watched::DeadlinePassed,
}
},
None => Watched::Ended(Box::new(watch(run_ctx, trainer, record).await)),
}
}
pub trait Lessee: Executor {
fn touch(&self, path: &str) -> impl Future<Output = Result<(), ExecError>> + Send;
}
impl Lessee for SshExecutor {
async fn touch(&self, path: &str) -> Result<(), ExecError> {
self.write_marker(path).await
}
}
impl Lessee for LocalExecutor {
fn touch(&self, path: &str) -> impl Future<Output = Result<(), ExecError>> + Send {
std::future::ready(touch_local(Path::new(path)))
}
}
fn touch_local(path: &Path) -> Result<(), ExecError> {
let io = |source| ExecError::Io {
path: path.to_path_buf(),
source,
};
if let Some(dir) = path.parent() {
fs::create_dir_all(dir).map_err(io)?;
}
fs::write(path, b"").map_err(io)
}
pub async fn watch_leased<E: Lessee, T: Trainer>(
run_ctx: &RunCtx<'_, E>,
trainer: &T,
record: RunRecord,
pod: &PodRecord,
) -> Watched {
if until_deadline(pod).is_none() {
return Watched::Ended(Box::new(watch(run_ctx, trainer, record).await));
}
let events = run_ctx.bus.subscribe();
let lease = format!("{}/{LEASE_FILE}", record.remote_dir);
tokio::select! {
outcome = watch(run_ctx, trainer, record) => Watched::Ended(Box::new(outcome)),
() = Box::pin(hold_lease(run_ctx.executor, &lease, events, pod)) => Watched::DeadlinePassed,
}
}
async fn hold_lease<E: Lessee>(
executor: &E,
path: &str,
mut events: Receiver<Event>,
pod: &PodRecord,
) {
let mut lease = Lease::new(pod);
let mut open = true;
let mut tick = tokio::time::interval(LEASE_TICK);
loop {
tokio::select! {
event = events.recv(), if open => match event {
Ok(event) => lease.saw(&event),
Err(RecvError::Lagged(_)) => {},
Err(RecvError::Closed) => open = false,
},
_ = tick.tick() => {
if lease.tick(executor, path).await {
return;
}
},
}
}
}
struct Lease<'a> {
pod: &'a PodRecord,
pace: Pace,
last_metric: SystemTime,
renewed: Option<SystemTime>,
warned: bool,
}
impl<'a> Lease<'a> {
fn new(pod: &'a PodRecord) -> Self {
Self {
pod,
pace: Pace::default(),
last_metric: SystemTime::now(),
renewed: None,
warned: false,
}
}
fn saw(&mut self, event: &Event) {
let Event::Metric(metric) = event else {
return;
};
self.pace.add(metric);
self.last_metric = SystemTime::now();
if self.warned {
return;
}
if let Some(note) = overrun(&self.pace, self.pod, self.last_metric, true) {
tracing::warn!("{note}");
self.warned = true;
}
}
async fn tick<E: Lessee>(&mut self, executor: &E, path: &str) -> bool {
let now = SystemTime::now();
if renewal_due(self.renewed, self.last_metric, now) {
match executor.touch(path).await {
Ok(()) => self.renewed = Some(now),
Err(error) => tracing::debug!("cannot renew the pod's lease: {error}"),
}
}
guard_fires(self.pod, self.renewed, now)
}
}
fn renewal_due(renewed: Option<SystemTime>, last_metric: SystemTime, now: SystemTime) -> bool {
let since = |at: SystemTime| now.duration_since(at).unwrap_or_default();
since(last_metric) < STALL && renewed.is_none_or(|at| since(at) >= LEASE_RENEW)
}
fn guard_fires(pod: &PodRecord, renewed: Option<SystemTime>, now: SystemTime) -> bool {
let held = renewed.is_some_and(|at| now.duration_since(at).unwrap_or_default() < LEASE_TTL);
!held && until_deadline_at(pod, now).is_some_and(|left| left.is_zero())
}
async fn warn_overrun(mut events: Receiver<Event>, pod: &PodRecord) {
let mut pace = Pace::default();
loop {
match events.recv().await {
Ok(Event::Metric(metric)) => {
pace.add(&metric);
if let Some(warning) = overrun(&pace, pod, SystemTime::now(), false) {
tracing::warn!("{warning}");
break;
}
},
Ok(_) | Err(RecvError::Lagged(_)) => {},
Err(RecvError::Closed) => break,
}
}
std::future::pending::<()>().await;
}
fn overrun(pace: &Pace, pod: &PodRecord, now: SystemTime, leased: bool) -> Option<String> {
if pod.keep {
return None;
}
let deadline = pod.deadline_unix?;
let left = pace.eta()?;
let end = now.checked_add(left)?.duration_since(UNIX_EPOCH).ok()?;
if end.as_secs() <= deadline {
return None;
}
let hours = left.as_secs_f64() / 3600.0;
let at = pod.deadline.as_deref().unwrap_or("its deadline");
if leased {
return Some(format!(
"at this pace the job needs about {hours:.1}h more, past max_hours ({at}): \
the pod stays while overbrainer follows the job; if it stops following, \
the pod's watchdog deletes the pod"
));
}
Some(format!(
"at this pace the job needs about {hours:.1}h more, but the pod's watchdog deletes it by {at} (max_hours): \
raise the target's max_hours, or train with --keep-pod, to let it finish"
))
}
pub async fn settle_watch(
ctx: &PodCtx<'_>,
pod: &mut PodRecord,
run_id: &str,
watched: Watched,
) -> Result<Outcome, PodError> {
match watched {
Watched::Ended(ended) => match *ended {
Ok(outcome) => Ok(outcome),
Err(error) => Err(unreachable(ctx, pod, run_id, error.into()).await),
},
Watched::DeadlinePassed => Err(deadline_reached(ctx, pod, run_id).await),
}
}
fn until_deadline(pod: &PodRecord) -> Option<Duration> {
until_deadline_at(pod, SystemTime::now())
}
pub(super) fn until_deadline_at(pod: &PodRecord, now: SystemTime) -> Option<Duration> {
if pod.keep {
return None;
}
let deadline = pod.deadline_unix?.saturating_add(DEADLINE_MARGIN.as_secs());
let now = now
.duration_since(UNIX_EPOCH)
.map_or(0, |since| since.as_secs());
Some(Duration::from_secs(deadline.saturating_sub(now)))
}
async fn deadline_reached(ctx: &PodCtx<'_>, pod: &mut PodRecord, run_id: &str) -> PodError {
if let Err(error) = remove(ctx, pod, DeleteReason::Deadline, DeletedBy::Client).await {
return error;
}
fail_run(ctx.runs, run_id, MAX_HOURS_REACHED);
PodError::DeadlineReached
}
async fn unreachable(
ctx: &PodCtx<'_>,
pod: &mut PodRecord,
run_id: &str,
error: PodError,
) -> PodError {
match gone(ctx, pod).await {
Ok(true) if past_deadline(pod, SystemTime::now()) => {
fail_run(ctx.runs, run_id, MAX_HOURS_REACHED);
PodError::DeadlineReached
},
Ok(true) => {
let id = pod.pod_id.clone();
if let Some(id) = &id {
fail_run(ctx.runs, run_id, &format!("pod {id} no longer exists"));
}
id.map_or(error, PodError::PodGone)
},
Ok(false) => error,
Err(check_error) => {
tracing::warn!("cannot look the pod up: {check_error}");
error
},
}
}
async fn gone(ctx: &PodCtx<'_>, pod: &mut PodRecord) -> Result<bool, PodError> {
let Some(id) = pod.pod_id.clone() else {
return Ok(true);
};
if pod.state == PodState::Deleted {
return Ok(true);
}
if ctx.client.get_pod(&id).await?.is_some() || look_again(ctx, &id).await?.is_some() {
return Ok(false);
}
mark_gone(ctx, pod, id, gone_by(pod, SystemTime::now()))?;
Ok(true)
}
#[must_use]
pub fn past_deadline(pod: &PodRecord, now: SystemTime) -> bool {
let now = now
.duration_since(UNIX_EPOCH)
.map_or(0, |since| since.as_secs());
!pod.keep && pod.deadline_unix.is_some_and(|deadline| now >= deadline)
}
fn gone_by(pod: &PodRecord, now: SystemTime) -> DeletedBy {
if past_deadline(pod, now) {
DeletedBy::Watchdog
} else {
DeletedBy::Unknown
}
}
pub(super) async fn look_up(ctx: &PodCtx<'_>, id: &PodId) -> Result<Option<Pod>, PodError> {
match ctx.client.get_pod(id).await? {
Some(pod) => Ok(Some(pod)),
None => look_again(ctx, id).await,
}
}
async fn look_again(ctx: &PodCtx<'_>, id: &PodId) -> Result<Option<Pod>, PodError> {
for _ in 1..GONE_LOOKS {
tokio::time::sleep(ctx.timing.gone_interval).await;
if let Some(pod) = ctx.client.get_pod(id).await? {
return Ok(Some(pod));
}
}
Ok(ctx
.client
.list_pods()
.await?
.into_iter()
.find(|listed| listed.id == *id))
}
#[derive(Debug)]
pub(super) enum Look {
Found(Box<Pod>),
Gone,
Failed(String),
}
pub(super) async fn look_up_all(ctx: &PodCtx<'_>, ids: &[PodId]) -> Vec<Look> {
let mut looks: Vec<Option<Look>> = ids.iter().map(|_| None).collect();
let mut pending: Vec<usize> = (0..ids.len()).collect();
for round in 0..GONE_LOOKS {
if pending.is_empty() {
break;
}
if round > 0 {
tokio::time::sleep(ctx.timing.gone_interval).await;
}
pending = look_round(ctx, ids, &pending, &mut looks).await;
}
look_again_all(ctx, ids, &pending, &mut looks).await;
looks
.into_iter()
.map(|look| {
look.unwrap_or_else(|| Look::Failed("the pod was not looked at".to_string()))
})
.collect()
}
async fn look_round(
ctx: &PodCtx<'_>,
ids: &[PodId],
pending: &[usize],
looks: &mut [Option<Look>],
) -> Vec<usize> {
let mut missing = Vec::new();
for &index in pending {
match ctx.client.get_pod(&ids[index]).await {
Ok(Some(pod)) => looks[index] = Some(Look::Found(Box::new(pod))),
Ok(None) => missing.push(index),
Err(error) => looks[index] = Some(Look::Failed(error.to_string())),
}
}
missing
}
async fn look_again_all(
ctx: &PodCtx<'_>,
ids: &[PodId],
pending: &[usize],
looks: &mut [Option<Look>],
) {
if pending.is_empty() {
return;
}
match ctx.client.list_pods().await {
Ok(listed) => {
for &index in pending {
looks[index] = Some(match listed.iter().find(|pod| pod.id == ids[index]) {
Some(pod) => Look::Found(Box::new(pod.clone())),
None => Look::Gone,
});
}
},
Err(error) => {
for &index in pending {
looks[index] = Some(Look::Failed(error.to_string()));
}
},
}
}
pub(super) fn mark_gone(
ctx: &PodCtx<'_>,
pod: &mut PodRecord,
id: PodId,
by: DeletedBy,
) -> Result<(), PodError> {
let now = SystemTime::now();
let uptime = pod.uptime(now);
pod.deleted(by, now);
pod.save(ctx.runs)?;
forget_client_key(ctx.runs, &pod.run_id);
ctx.bus.publish(Event::PodStatus(PodStatus::Deleted {
pod_id: id,
uptime,
estimated_spend: pod.estimated_spend,
}));
Ok(())
}
fn fail_run(runs: &Runs, run_id: &str, message: &str) {
let result = runs.load(run_id).and_then(|mut run| {
run.state = RunState::Failed;
run.message = Some(message.to_string());
runs.save(&run)
});
if let Err(error) = result {
tracing::warn!("cannot record run {run_id} as failed: {error}");
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Ending {
Deleted,
Kept,
AwaitingRetrieval,
}
pub async fn end_pod(
ctx: &PodCtx<'_>,
pod: &mut PodRecord,
executor: &SshExecutor,
run: &RunRecord,
retrieved: bool,
) -> Result<Ending, PodError> {
fetch_watchdog_log(ctx.runs, executor, run).await;
if !retrieved {
return unretrieved(ctx, pod, &run.id);
}
let marker = format!("{}/{RETRIEVED_MARKER}", run.remote_dir);
if let Err(error) = executor.write_marker(&marker).await {
tracing::warn!(
"cannot mark the results of run {} retrieved on the pod: {error}",
run.id
);
}
if pod.keep {
pod.state = PodState::Kept;
pod.save(ctx.runs)?;
if let Some(id) = &pod.pod_id {
ctx.bus
.publish(Event::PodStatus(PodStatus::Kept { pod_id: id.clone() }));
}
return Ok(Ending::Kept);
}
remove(ctx, pod, DeleteReason::Retrieved, DeletedBy::Client).await?;
let stray = sweep(ctx, &run.id, None).await;
note_strays(ctx, pod, stray);
forget_client_key(ctx.runs, &run.id);
Ok(Ending::Deleted)
}
fn unretrieved(ctx: &PodCtx<'_>, pod: &mut PodRecord, run_id: &str) -> Result<Ending, PodError> {
let name = pod
.pod_id
.as_ref()
.map_or_else(|| "(none)".to_string(), ToString::to_string);
let (stays, ending) = if pod.keep {
pod.state = PodState::Kept;
(
"stays (--keep-pod), nothing deletes it automatically",
Ending::Kept,
)
} else {
pod.state = PodState::AwaitingRetrieval;
(
"stays until its watchdog's retrieve grace ends",
Ending::AwaitingRetrieval,
)
};
pod.save(ctx.runs)?;
tracing::warn!(
"the results of run {run_id} were not retrieved: pod {name} {stays}; retrieve them with `overbrainer train attach {run_id}`, or remove the pod with `overbrainer pod rm {run_id}`"
);
Ok(ending)
}
pub const WATCHDOG_LOG: &str = ".pod/watchdog.log";
async fn fetch_watchdog_log(runs: &Runs, executor: &SshExecutor, run: &RunRecord) {
let Ok(local) = runs.run_dir(&run.id) else {
return;
};
let entries = [WATCHDOG_LOG.to_string()];
if let Err(error) = executor
.download(&run.remote_dir, &local, &entries, &[])
.await
{
tracing::warn!("cannot copy the watchdog's log of run {}: {error}", run.id);
}
}
pub fn forget_client_key(runs: &Runs, run_id: &str) {
let Ok(dir) = runs.run_dir(run_id) else {
return;
};
let key = dir.join(SSH_DIR).join(CLIENT_KEY);
match fs::remove_file(&key) {
Ok(()) => {},
Err(e) if e.kind() == io::ErrorKind::NotFound => {},
Err(error) => tracing::warn!("cannot remove {}: {error}", key.display()),
}
}
pub async fn reconnect(
ctx: &PodCtx<'_>,
pod: &mut PodRecord,
run: &RunRecord,
) -> Result<Option<SshExecutor>, PodError> {
let Some(id) = pod.pod_id.clone() else {
return Ok(None);
};
if pod.state == PodState::Deleted {
return Ok(None);
}
let Some(remote) = look_up(ctx, &id).await? else {
mark_gone(ctx, pod, id, gone_by(pod, SystemTime::now()))?;
return Ok(None);
};
if remote.status == RemoteStatus::Exited {
return Err(PodError::PodStopped {
pod_id: id,
run_id: run.id.clone(),
});
}
let direct = remote
.direct()
.ok_or_else(|| PodError::NoEndpoint(id.clone(), remote.status.name().to_string()))?;
let endpoint = SshEndpoint {
host: direct.host.clone(),
port: direct.port,
user: direct.username.clone(),
};
let ssh_dir = ctx.runs.run_dir(&run.id)?.join(SSH_DIR);
let keys = PodKeys::new(
ssh_dir.join(CLIENT_KEY),
String::new(),
pod.host_key.clone(),
SecretString::from(String::new()),
);
let alias = alias(&run.id);
let config = write_config(&ssh_dir, &alias, &endpoint, &keys)?;
let workdir = run
.remote_dir
.rsplit_once('/')
.map_or(run.remote_dir.as_str(), |(parent, _)| parent);
let executor = SshExecutor::connect(&alias, workdir, Some(&config)).await?;
pod.ssh = Some(endpoint);
pod.save(ctx.runs)?;
Ok(Some(executor))
}
#[must_use]
pub fn ssh_command(run_id: &str) -> String {
format!(
"ssh -F runs/{run_id}/{SSH_DIR}/{} {}",
super::SSH_CONFIG,
alias(run_id)
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::runpod::ApiError;
use crate::train::TrainMetric;
fn slow_pace() -> Pace {
let mut pace = Pace::default();
for (time, step) in [(0.0, 90), (100.0, 100)] {
pace.add(&TrainMetric {
time,
step,
epoch: None,
max_steps: Some(7300),
loss: Some(1.0),
eval_loss: None,
learning_rate: None,
grad_norm: None,
});
}
pace
}
fn pod_due_in(hours: u64, keep: bool, now: SystemTime) -> PodRecord {
let mut pod = PodRecord::new("r1", keep, 1, "ssh-ed25519 AAAAhost");
let deadline = now + Duration::from_secs(hours * 3600);
pod.deadline_unix = deadline
.duration_since(UNIX_EPOCH)
.ok()
.map(|since| since.as_secs());
pod.deadline = Some("2026-09-29T19:54:50Z".into());
pod
}
#[test]
fn a_job_that_cannot_end_before_the_deadline_is_warned_about() {
let now = SystemTime::now();
assert_eq!(
overrun(&slow_pace(), &pod_due_in(6, false, now), now, false).as_deref(),
Some(
"at this pace the job needs about 20.0h more, but the pod's watchdog deletes it by \
2026-09-29T19:54:50Z (max_hours): raise the target's max_hours, or train with \
--keep-pod, to let it finish"
)
);
}
#[test]
fn a_followed_job_past_the_deadline_is_only_noted() {
let now = SystemTime::now();
let note = overrun(&slow_pace(), &pod_due_in(6, false, now), now, true);
assert_eq!(
note.as_deref(),
Some(
"at this pace the job needs about 20.0h more, past max_hours \
(2026-09-29T19:54:50Z): the pod stays while overbrainer follows the job; \
if it stops following, the pod's watchdog deletes the pod"
)
);
}
#[test]
fn the_watchdog_holds_the_lease_as_long_as_the_client_counts_on() {
let default = format!("OVERBRAINER_LEASE_TTL:-{}}}", LEASE_TTL.as_secs());
assert!(super::super::watchdog_script().contains(&default));
}
#[test]
fn the_lease_is_renewed_every_few_minutes_while_the_job_progresses() {
let now = SystemTime::now();
let ago = |minutes: u64| now - Duration::from_secs(minutes * 60);
assert!(renewal_due(None, now, now));
assert!(!renewal_due(Some(ago(1)), ago(1), now));
assert!(renewal_due(Some(ago(5)), ago(1), now));
assert!(!renewal_due(Some(ago(5)), ago(30), now));
assert!(!renewal_due(None, ago(31), now));
}
#[test]
fn the_client_guard_waits_for_the_lease_to_run_out() {
let now = SystemTime::now();
let ago = |minutes: u64| Some(now - Duration::from_secs(minutes * 60));
let late = pod_due_in(0, false, now - Duration::from_secs(3600));
assert!(!guard_fires(&late, ago(1), now));
assert!(!guard_fires(&late, ago(14), now));
assert!(guard_fires(&late, ago(15), now));
assert!(guard_fires(&late, None, now));
assert!(!guard_fires(&pod_due_in(1, false, now), None, now));
assert!(!guard_fires(
&pod_due_in(0, true, now - Duration::from_secs(3600)),
None,
now
));
}
#[test]
fn no_overrun_in_time_for_a_kept_pod_or_without_a_pace() {
let now = SystemTime::now();
assert_eq!(
overrun(&slow_pace(), &pod_due_in(21, false, now), now, false),
None
);
assert_eq!(
overrun(&slow_pace(), &pod_due_in(6, true, now), now, false),
None
);
assert_eq!(
overrun(&Pace::default(), &pod_due_in(6, false, now), now, false),
None
);
}
#[test]
fn a_failed_run_keeps_only_the_status_of_an_api_answer() {
let answered = PodError::Api(ApiError::Status {
status: 500,
message: "server text".into(),
retry_after: None,
capacity: false,
});
assert_eq!(run_message(&answered), "Runpod answered 500");
assert_eq!(
run_message(&PodError::Interrupted),
"interrupted before the job started"
);
}
}