use serde::{Deserialize, Serialize};
mod admission;
mod budget;
mod control;
mod events;
mod insights;
mod jobs;
mod records;
mod results;
mod savings;
mod sizing;
mod slots;
mod stats;
mod storage;
mod tone;
mod units;
pub use admission::{Admission, AdmissionPolicy, Basis, CpuAdmission, JobPeak, cores, cpus};
pub use budget::{
BudgetParts, ClosedKind, ClosedPart, FreePart, HeldKind, HeldPart, Reading, SlotPart,
};
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, ChartLine, ChartLineKind,
ChartLines, Holder, HostMemory, HostSample, HostSampleQuery, HostSamples, Job, JobMetrics,
JobPage, JobQuery, JobRunner, JobState, JobSummary, JobTitle, Limit, LimitSource, Limits,
LogQuery, LogSummary, Milestone, Outcome, OutcomeKind, Phase, PhaseKind, Pressure,
RunComparison, Sample, SampleSeries, SampleUsage, Started, Stats, StatsQuery, StatsRow, Step,
Timeline, job_title,
};
pub use records::{
Record, RecordLimits, RecordNote, RecordRemoval, Records, RemoveRecordsQuery, RemovedRecords,
SHELL_PROTOCOL, ShellQuery,
};
pub use results::{force_pause_consequence, reseed_requested, reseeded};
pub use savings::{
Period, PricedRun, PricedRunner, Pricing, PricingHistory, SELF_HOSTED_RATE, SavedWindow,
Savings, Sku, Usd, Visibility, billed_minutes, reads_since,
};
pub use sizing::{
Advice, AutoPlan, GRANULE, JobSizing, Need, PAGE_CACHE_ROOM, Peaks, ProfilePlan, ProfileSizing,
Resize, RevertCause, SAMPLE_INTERVAL, Sizing, SizingChange, SizingHold, SizingQuery,
SizingRefused, SizingRevert, Spread, Starts, need, period, sampled_note,
};
pub use slots::{Health, SlotState};
pub use stats::StatsSummary;
pub use storage::{
BuildCacheActivity, CacheLimit, RECORDS_NEAR_FULL, RecordState, records_near_full,
};
pub use tone::{Tone, Toned};
#[cfg(feature = "host")]
pub(crate) use units::days_from_civil;
pub use units::{ago, cores_used, cpu_time, duration, in_cores};
#[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>,
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,
pub admission: AdmissionPolicy,
pub held: bool,
pub mirror_memory: u64,
pub seed_memory: Vec<SeedMemory>,
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, PartialEq, Eq, Serialize, Deserialize)]
pub struct HostBudget {
pub memory: HostMemory,
pub limit: u64,
pub spare: u64,
pub stalled: bool,
}
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,
pub reserved: u64,
pub warm: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RepositoryStatus {
pub name: String,
pub pools: Vec<PoolStatus>,
pub paused: bool,
pub queued_jobs: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PoolStatus {
pub profile: String,
pub labels: Vec<String>,
pub queued_jobs: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct QueuedJob {
pub id: u64,
pub repository: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub profile: Option<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>,
}
fn starting_for(job: &QueuedJob, status: &Status) -> Option<String> {
status
.profiles
.iter()
.filter(|profile| job.profile.as_ref().is_none_or(|own| *own == profile.name))
.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))
})
}
impl QueuedJob {
pub fn place(&self, status: &Status) -> String {
let pools = status
.repositories
.iter()
.find(|repository| repository.name == self.repository)
.map_or(0, |repository| repository.pools.len());
match self.profile.as_deref().filter(|_| pools > 1) {
Some(profile) => format!("{}\u{a0}· {profile}", self.repository),
None => self.repository.clone(),
}
}
}
impl RepositoryStatus {
pub fn first_profile(&self) -> &str {
self.pools.first().map_or("", |pool| pool.profile.as_str())
}
pub fn profiles(&self) -> String {
self.pools
.iter()
.map(|pool| pool.profile.as_str())
.collect::<Vec<_>>()
.join(" + ")
}
pub fn pool_for(&self, labels: &[String]) -> Option<String> {
self.pools
.iter()
.find(|pool| crate::config::labels_match(labels, &pool.labels))
.map(|pool| pool.profile.clone())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum HoldCause {
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,
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,
},
NoPool {
labels: Vec<String>,
},
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WaitKind {
Act,
Soon,
Full,
Other,
}
impl Waiting {
pub fn kind(&self) -> WaitKind {
match self {
Waiting::Paused | Waiting::Backoff { .. } | Waiting::NoPool { .. } => WaitKind::Act,
Waiting::Unknown => WaitKind::Other,
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::Unknown => "Waits for a reason this client does not know yet".to_owned(),
Waiting::NoPool { labels } if labels.is_empty() => {
"It asks for no labels, so no profile serves it".to_owned()
}
Waiting::NoPool { labels } => format!(
"No profile serves the {} {}",
if labels.len() == 1 { "label" } else { "labels" },
labels.join(", ")
),
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",
},
duration(until.saturating_sub(now))
),
Waiting::RunnerStarting => match starting_for(job, status) {
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 { needs, free } => {
let host = status.budget.host.as_ref();
let reason = if host.is_some_and(|host| host.stalled) {
"The host is short of memory: its programs stall waiting for it, so nothing starts"
.to_owned()
} else {
format!(
"The host is short of memory: needs {}; the host has {} to spare",
size(*needs),
size(*free)
)
};
match host {
Some(host) => format!("{reason}; {}", host.memory.outside_line()),
None => reason,
}
}
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,
}