use std::collections::BTreeMap;
use std::time::{Duration, SystemTime};
use tracing::{info, warn};
use super::{Engine, Occupant, RunningJobs, unix_secs};
use crate::cache::{self, runner_mounts};
use crate::docker::{DockerApi, RunnerSpec, is_not_found};
use crate::github::{Deleted, GithubApi};
use crate::scheduler::SlotRef;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Release {
Ended,
Idle,
}
pub(super) fn backoff(failures: u32) -> Duration {
Duration::from_secs(30 << failures.saturating_sub(1).min(5)).min(Duration::from_secs(600))
}
impl<G: GithubApi + 'static, D: DockerApi + 'static> Engine<G, D> {
pub(super) async fn collect_finished(&mut self) {
let occupied: Vec<(SlotRef, Occupant)> = self
.occupied()
.map(|(slot, occupant)| (slot, occupant.clone()))
.collect();
for (slot, occupant) in occupied {
let running = match self.docker.inspect(&occupant.container).await {
Ok(state) => state.running,
Err(error) if is_not_found(&error) => false,
Err(error) => {
warn!("cannot inspect {}: {error}", occupant.runner_name);
continue;
}
};
if !running {
info!("{}: {} ended", occupant.repository, occupant.runner_name);
self.release(&slot, &occupant, Release::Ended).await;
}
}
}
async fn release(&mut self, slot: &SlotRef, occupant: &Occupant, why: Release) {
let took_job = if why == Release::Ended {
match self.docker.logs(&occupant.container).await {
Ok(logs) => Some(logs.contains("Running job: ")),
Err(error) => {
warn!("cannot read the log of {}: {error}", occupant.runner_name);
None
}
}
} else {
None
};
self.end_capture(&occupant.runner_name).await;
self.count_mirror_bytes().await;
match self.docker.remove(&occupant.container).await {
Err(error) if !is_not_found(&error) => {
warn!("cannot remove {}: {error}", occupant.runner_name);
return;
}
_ => {}
}
match self
.github
.delete_runner(&occupant.repository, occupant.runner_id)
.await
{
Ok(_) => {}
Err(error) => warn!("cannot delete {}: {error}", occupant.runner_name),
}
self.forget(occupant);
if let Some(state) = self.repositories.get_mut(&occupant.repository) {
match took_job {
Some(true) => {
state.failures = 0;
state.held_until = None;
}
Some(false) => {
warn!(
"{}: {} ended without taking a job",
occupant.repository, occupant.runner_name
);
self.hold(&occupant.repository);
}
None => {}
}
}
self.vacate(slot);
}
fn vacate(&mut self, slot: &SlotRef) {
if let Some(entry) = self
.slots
.get_mut(&slot.profile)
.and_then(|slots| slots.get_mut(slot.index))
{
*entry = None;
}
if slot.index == 0 {
self.reseed_due.insert(slot.profile.clone());
}
self.trim_due = true;
self.drop_closed_slots();
}
pub(super) async fn stop(&mut self, slot: &SlotRef, jobs: RunningJobs) {
let Some(occupant) = self
.slots
.get(&slot.profile)
.and_then(|slots| slots.get(slot.index))
.cloned()
.flatten()
else {
return;
};
if jobs == RunningJobs::Kill {
info!("{}: killing {}", occupant.repository, occupant.runner_name);
if let Err(error) = self.docker.remove(&occupant.container).await
&& !is_not_found(&error)
{
warn!("cannot remove {}: {error}", occupant.runner_name);
return;
}
self.end_capture(&occupant.runner_name).await;
self.count_mirror_bytes().await;
self.forget(&occupant);
match self
.github
.delete_runner(&occupant.repository, occupant.runner_id)
.await
{
Ok(Deleted::Busy) => warn!(
"{}: GitHub keeps {} until it notices its job is gone; the next runnerd start deletes it",
occupant.repository, occupant.runner_name
),
Ok(_) => {}
Err(error) => warn!("cannot delete {}: {error}", occupant.runner_name),
}
self.vacate(slot);
return;
}
match self
.github
.delete_runner(&occupant.repository, occupant.runner_id)
.await
{
Ok(Deleted::Busy) => {
if let Some(Some(entry)) = self
.slots
.get_mut(&slot.profile)
.and_then(|slots| slots.get_mut(slot.index))
{
entry.busy = true;
}
}
Ok(Deleted::Deleted | Deleted::NotFound) => {
info!(
"{}: removed idle {}",
occupant.repository, occupant.runner_name
);
self.release(slot, &occupant, Release::Idle).await;
}
Err(error) => warn!("cannot delete {}: {error}", occupant.runner_name),
}
}
fn hold(&mut self, repository: &str) {
if let Some(state) = self.repositories.get_mut(repository) {
state.failures += 1;
let wait = backoff(state.failures);
state.held_until = Some(SystemTime::now() + wait);
warn!("{repository}: no runner starts for {}s", wait.as_secs());
}
}
pub(super) async fn start(&mut self, slot: &SlotRef, repository: &str) {
let Some(profile) = self.config.profiles.get(&slot.profile) else {
return;
};
let Some(labels) = self
.config
.repositories
.iter()
.find(|candidate| candidate.name == repository)
.map(|candidate| candidate.labels.clone())
else {
return;
};
let number = slot.index + 1;
let now = SystemTime::now();
let name = self
.identity
.runner_name(&slot.profile, number, unix_secs(now));
if let Err(error) = cache::prepare(&*self.docker, &self.names, &slot.profile, number).await
{
warn!(
"cannot prepare the caches of {} slot {number}: {error}",
slot.profile
);
return;
}
let mirror = &self.config.caches.mirror;
if mirror.enabled && !self.mirror_up {
match cache::mirror_up(&*self.docker, &self.names, mirror, &self.config.host.slice)
.await
{
Ok(()) => {
self.mirror_up = true;
self.mirror_memory = mirror.memory.0;
}
Err(error) => warn!("cannot start the mirror: {error}"),
}
}
let (runner, jit_config) = match self.github.jit_config(repository, &name, &labels).await {
Ok(registered) => registered,
Err(error) => {
warn!("{repository}: cannot register a runner: {error}");
self.hold(repository);
return;
}
};
let spec = RunnerSpec {
name: name.clone(),
image: profile.image.reference(),
profile: slot.profile.clone(),
slice: self.config.host.slice.clone(),
cpus: profile.cpus,
memory: profile.memory,
env: Vec::new(),
mounts: runner_mounts(&self.names, &slot.profile, number),
network: Some(self.names.network()),
labels: BTreeMap::from([
("runnerd.job".to_owned(), name.clone()),
("runnerd.repository".to_owned(), repository.to_owned()),
("runnerd.slot".to_owned(), number.to_string()),
("runnerd.runner-id".to_owned(), runner.id.to_string()),
]),
entrypoint: None,
jit_config: Some(jit_config),
};
match self.docker.start(&spec, self.isolation).await {
Ok(container) => {
info!(
"{repository}: started {name} in {} slot {number}",
slot.profile
);
let occupant = Occupant {
repository: repository.to_owned(),
runner_name: name,
runner_id: runner.id,
container,
busy: false,
started_at: now,
memory: profile.memory.0,
job: None,
};
if let Some(entry) = self
.slots
.get_mut(&slot.profile)
.and_then(|slots| slots.get_mut(slot.index))
{
*entry = Some(occupant.clone());
}
self.observe(slot, &occupant).await;
}
Err(error) => {
warn!("{repository}: cannot start {name}: {error}");
self.recheck_image(&spec.image);
if let Err(error) = self.github.delete_runner(repository, runner.id).await {
warn!("{repository}: cannot delete {name}: {error}");
}
self.hold(repository);
}
}
}
}