use serde::{Deserialize, Serialize};
mod admission;
mod control;
mod events;
mod insights;
mod jobs;
mod records;
mod sizing;
pub use admission::{Admission, AdmissionPolicy, Basis, CpuAdmission, JobPeak, cores, cpus};
pub use control::{
Busy, Check, CheckStatus, ConfigCheck, ConfigFile, Doctor, PauseQuery, Paused, ReplaceQuery,
RunningJob,
};
pub use events::{Event, EventEnvelope, EventQuery, RunnerFailure};
pub use insights::{
AutoNote, Delivery, Digest, INSIGHT_DAYS, INSIGHT_DAYS_MAX, Insight, InsightKind, Insights,
InsightsQuery, LongWait, Notification, NotifyEvent, NotifyTest, NotifyTestQuery, Priority,
RepositoryWaits, RunLink, Severity, WaitSpread, WeekWaits,
};
pub use jobs::{
Activity, ActivityQuery, Bucket, BuildCacheCounts, BuildCacheStats, CacheHistory,
CacheHistoryQuery, CacheKind, CacheSize, CacheVolume, Caches, HostMemory, HostSample,
HostSampleQuery, HostSamples, Job, JobMetrics, JobPage, JobQuery, JobRunner, JobState,
JobSummary, LogQuery, LogSummary, Pressure, Sample, SampleSeries, Started, Stats, StatsQuery,
StatsRow, Step, Timeline,
};
pub use records::{
Record, RecordLimits, RecordNote, RecordRemoval, Records, RemoveRecordsQuery, RemovedRecords,
SHELL_PROTOCOL, ShellQuery,
};
pub use sizing::{
Advice, AutoPlan, JobSizing, PAGE_CACHE_ROOM, Peaks, ProfileSizing, Resize, Sizing,
SizingChange, SizingHold, SizingQuery, Spread, Starts, period,
};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", tag = "kind")]
pub enum Principal {
LocalOwner {
uid: u32,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Status {
pub version: String,
pub shutting_down: bool,
pub budget: Budget,
pub profiles: Vec<ProfileStatus>,
pub repositories: Vec<RepositoryStatus>,
#[serde(default)]
pub queued: Vec<QueuedJob>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Budget {
pub slice: String,
pub memory_max: Option<u64>,
pub memory_in_use: u64,
#[serde(default)]
pub admission: AdmissionPolicy,
#[serde(default)]
pub held: bool,
#[serde(default)]
pub mirror_memory: u64,
#[serde(default)]
pub seed_memory: Vec<SeedMemory>,
#[serde(default)]
pub shell_memory: Vec<ShellMemory>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub host: Option<HostBudget>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cpu: Option<CpuBudget>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub slice_cpus: Option<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct HostBudget {
pub memory: HostMemory,
pub limit: u64,
pub spare: u64,
pub stalled: bool,
}
impl HostBudget {
pub fn line(&self) -> String {
let size = |bytes: u64| crate::config::ByteSize(bytes).to_string();
let mut line = format!(
"host: {} of {} available",
size(self.memory.available),
size(self.memory.total)
);
if let Some(full) = self.memory.full.filter(|full| full.avg10 > 0) {
line = format!("{line}, {} stalled for memory", percent(full.avg10));
}
if self.stalled {
format!("{line}; stalling, so no runner starts")
} else {
format!("{line}; leaves the slice {}", size(self.limit))
}
}
}
pub fn percent(hundredths: u32) -> String {
let whole = hundredths / 100;
match hundredths % 100 / 10 {
0 => format!("{whole}%"),
tenth => format!("{whole}.{tenth}%"),
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct CpuBudget {
pub cpus: u64,
pub budget: u64,
pub in_use: u64,
pub held: bool,
}
impl CpuBudget {
pub fn line(&self) -> String {
let mut line = format!(
"CPU {} of {} CPUs reserved",
cores(self.in_use),
cores(self.budget)
);
if self.held {
line.push_str("; a job waits for CPU");
}
line
}
}
impl Budget {
pub fn admission_note(&self) -> Option<&'static str> {
match (self.admission, self.held) {
(AdmissionPolicy::Caps, _) => None,
(AdmissionPolicy::Measured, false) => Some("by predicted peaks"),
(AdmissionPolicy::Measured, true) => Some(
"by predicted peaks; the slice is past 90% of its MemoryMax, so no runner starts",
),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ShellMemory {
pub job: u64,
pub memory: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SeedMemory {
pub profile: String,
pub memory: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProfileStatus {
pub name: String,
pub image: String,
pub cpus: u32,
pub memory: u64,
pub reseeding: bool,
pub slots: Vec<SlotStatus>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SlotStatus {
pub slot: usize,
pub runner: Option<RunnerStatus>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RunnerStatus {
pub repository: String,
pub name: String,
pub runner_id: u64,
pub container: String,
pub busy: bool,
pub started_at: u64,
#[serde(default)]
pub reserved: u64,
#[serde(default)]
pub warm: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RepositoryStatus {
pub name: String,
pub profile: String,
pub labels: Vec<String>,
pub paused: bool,
pub queued_jobs: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct QueuedJob {
pub id: u64,
pub repository: String,
pub run_id: u64,
pub run_attempt: u32,
pub workflow: Option<String>,
pub name: String,
pub branch: Option<String>,
pub url: Option<String>,
pub queued_at: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub waiting: Option<Waiting>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum HoldCause {
#[default]
NoJob,
IdleExits,
Register,
Start,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", tag = "kind")]
pub enum Waiting {
Paused,
Backoff {
until: u64,
#[serde(default)]
cause: HoldCause,
},
RunnerStarting,
Reseeding {
profile: String,
},
PullingImage {
profile: String,
},
NoSlot {
profile: String,
},
Memory {
needs: u64,
free: u64,
},
HostMemory {
needs: u64,
free: u64,
},
Cpu {
needs: u64,
free: u64,
},
BehindCpu {
job: u64,
},
BehindStart {
job: u64,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WaitKind {
Act,
Soon,
Full,
}
impl Waiting {
pub fn kind(&self) -> WaitKind {
match self {
Waiting::Paused | Waiting::Backoff { .. } => WaitKind::Act,
Waiting::RunnerStarting
| Waiting::Reseeding { .. }
| Waiting::PullingImage { .. }
| Waiting::BehindStart { .. } => WaitKind::Soon,
Waiting::NoSlot { .. }
| Waiting::Memory { .. }
| Waiting::HostMemory { .. }
| Waiting::Cpu { .. }
| Waiting::BehindCpu { .. } => WaitKind::Full,
}
}
pub fn reason(&self, job: &QueuedJob, status: &Status, now: u64) -> String {
let size = |bytes: u64| crate::config::ByteSize(bytes).to_string();
let older_name = |id: u64| {
status
.queued
.iter()
.find(|candidate| candidate.id == id)
.map_or_else(|| "an older job".to_owned(), |older| older.name.clone())
};
match self {
Waiting::Paused => format!("{} is paused", job.repository),
Waiting::Backoff { until, cause } => format!(
"{}; retrying in {}",
match cause {
HoldCause::NoJob => "Runners failed to take a job",
HoldCause::IdleExits => "Runners ended without a job after waiting for one",
HoldCause::Register => "Runners cannot register with GitHub",
HoldCause::Start => "Runners cannot start",
},
admission::wait(until.saturating_sub(now))
),
Waiting::RunnerStarting => {
let slot = status.profiles.iter().find_map(|profile| {
profile
.slots
.iter()
.find(|slot| {
slot.runner.as_ref().is_some_and(|runner| {
runner.repository == job.repository && !runner.busy
})
})
.map(|slot| format!("{}\u{a0}{}", profile.name, slot.slot))
});
match slot {
Some(slot) => format!("A runner is starting for it in {slot}"),
None => "A runner is starting for it".to_owned(),
}
}
Waiting::Reseeding { profile } => format!("{profile} is rebuilding its snapshot"),
Waiting::PullingImage { profile } => format!("Pulling {profile}'s image"),
Waiting::NoSlot { profile } => {
let slots = status
.profiles
.iter()
.find(|candidate| &candidate.name == profile)
.map_or(0, |profile| profile.slots.len());
if slots == 1 {
format!("The {profile} slot is busy")
} else {
format!("All {slots} {profile} slots are busy")
}
}
Waiting::Memory { .. } if status.budget.held => {
"The slice is past 90% of its MemoryMax, so nothing starts".to_owned()
}
Waiting::Memory { needs, free } => format!(
"Needs {} of memory; {} of the budget is free",
size(*needs),
size(*free)
),
Waiting::HostMemory { .. }
if status.budget.host.as_ref().is_some_and(|host| host.stalled) =>
{
"The host is short of memory: its programs stall waiting for it, so nothing starts"
.to_owned()
}
Waiting::HostMemory { needs, free } => format!(
"The host is short of memory: needs {}; the host has {} to spare",
size(*needs),
size(*free)
),
Waiting::Cpu { needs, free } => {
format!(
"Needs {}; {} are free",
cpus(*needs),
admission::free_cores(*free)
)
}
Waiting::BehindCpu { job: older } => {
format!("Behind {}, which waits for CPU", older_name(*older))
}
Waiting::BehindStart { job: older } => {
format!(
"Next after {}, whose runner starts first",
older_name(*older)
)
}
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Dashboard {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub open: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct PairingQuery {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub origin: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Pairings {
pub links: Vec<Pairing>,
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Pairing {
pub origin: String,
pub url: String,
pub lifetime: u64,
}
impl std::fmt::Debug for Pairing {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("Pairing")
.field("origin", &self.origin)
.field("lifetime", &self.lifetime)
.finish_non_exhaustive()
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ShutdownRequest {
#[serde(default)]
pub force: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ErrorResponse {
pub message: String,
}