Skip to main content

runnerd_core/api/
slots.rs

1//! What a slot is doing and whether CI keeps up, as every client names and counts it. Plain derivations of
2//! [`Status`] and [`Activity`]: a client never infers a slot's state from whether it found the job's details.
3
4use super::{Activity, ProfileStatus, RunnerStatus, SlotStatus, Status, Tone};
5
6/// What a slot is doing, from `Status::slot_state`.
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub enum SlotState {
9    /// No runner, and nothing to do.
10    Idle,
11    /// No runner: slot 1's layers become the profile's snapshot.
12    Reseeding,
13    /// A runner started for a queued job and waits for GitHub to hand it one.
14    Starting {
15        /// `owner/name` the runner is registered on.
16        repository: String,
17    },
18    /// A warm runner: no queued job waits for it; it takes the next one GitHub hands out.
19    Warm {
20        /// `owner/name` the runner is registered on.
21        repository: String,
22    },
23    /// Its runner runs a job.
24    Running {
25        /// `owner/name` the runner is registered on.
26        repository: String,
27    },
28}
29
30impl SlotState {
31    /// What a slot holding `runner` is doing: running its job, warm for its repository, or starting.
32    pub fn of_runner(runner: &RunnerStatus) -> SlotState {
33        let repository = runner.repository.clone();
34        if runner.busy {
35            SlotState::Running { repository }
36        } else if runner.warm {
37            SlotState::Warm { repository }
38        } else {
39            SlotState::Starting { repository }
40        }
41    }
42
43    /// One word for it, as a table column says it: `idle`, `reseeding`, `starting`, `warm` or `running`.
44    pub fn word(&self) -> &'static str {
45        match self {
46            SlotState::Idle => "idle",
47            SlotState::Reseeding => "reseeding",
48            SlotState::Starting { .. } => "starting",
49            SlotState::Warm { .. } => "warm",
50            SlotState::Running { .. } => "running",
51        }
52    }
53
54    /// What it does, as a sentence's tail: `idle`, `reseeding the snapshot`, `a runner for o/app waiting for its
55    /// job`, `a warm runner for o/app, waiting for a job` or `running a job of o/app`.
56    pub fn line(&self) -> String {
57        match self {
58            SlotState::Idle => "idle".to_owned(),
59            SlotState::Reseeding => "reseeding the snapshot".to_owned(),
60            SlotState::Starting { repository } => {
61                format!("a runner for {repository} waiting for its job")
62            }
63            SlotState::Warm { repository } => {
64                format!("a warm runner for {repository}, waiting for a job")
65            }
66            SlotState::Running { repository } => format!("running a job of {repository}"),
67        }
68    }
69
70    /// Whether a runner runs a job in it.
71    pub fn is_running(&self) -> bool {
72        matches!(self, SlotState::Running { .. })
73    }
74
75    /// What it means at a glance: a running slot is a plain fact, every other one background.
76    pub fn tone(&self) -> Tone {
77        match self {
78            SlotState::Running { .. } => Tone::Neutral,
79            _ => Tone::Quiet,
80        }
81    }
82}
83
84impl Status {
85    /// What `slot` of `profile` is doing: its runner's state as the daemon reports it, or for a free slot, whether its
86    /// profile reseeds into it.
87    pub fn slot_state(profile: &ProfileStatus, slot: &SlotStatus) -> SlotState {
88        match &slot.runner {
89            Some(runner) => SlotState::of_runner(runner),
90            None if profile.reseeding && slot.slot == 1 => SlotState::Reseeding,
91            None => SlotState::Idle,
92        }
93    }
94
95    /// Whether CI keeps up now: slots running, starting and warm, and the queue with its oldest job's wait at `now`.
96    pub fn health(&self, now: u64) -> Health {
97        let mut health = Health {
98            slots: 0,
99            running: 0,
100            starting: 0,
101            warm: 0,
102            waiting: self.queued.len(),
103            oldest_wait: self
104                .queued
105                .iter()
106                .filter(|job| job.queued_at > 0)
107                .map(|job| now.saturating_sub(job.queued_at))
108                .max(),
109        };
110        for profile in &self.profiles {
111            for slot in &profile.slots {
112                health.slots += 1;
113                match Status::slot_state(profile, slot) {
114                    SlotState::Running { .. } => health.running += 1,
115                    SlotState::Starting { .. } => health.starting += 1,
116                    SlotState::Warm { .. } => health.warm += 1,
117                    SlotState::Idle | SlotState::Reseeding => {}
118                }
119            }
120        }
121        health
122    }
123}
124
125/// Whether CI keeps up now, from `Status::health`.
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub struct Health {
128    /// Every configured slot.
129    pub slots: usize,
130    /// Slots whose runner runs a job.
131    pub running: usize,
132    /// Slots whose runner waits for the queued job it started for.
133    pub starting: usize,
134    /// Slots whose warm runner waits for jobs not queued yet.
135    pub warm: usize,
136    /// Queued jobs no runner holds yet.
137    pub waiting: usize,
138    /// How long the oldest of them has waited, in seconds; `None` when none says when it was queued.
139    pub oldest_wait: Option<u64>,
140}
141
142impl Health {
143    /// What the queue means: any job waiting is worth a look.
144    pub fn waiting_tone(&self) -> Tone {
145        if self.waiting > 0 {
146            Tone::Warn
147        } else {
148            Tone::Neutral
149        }
150    }
151}
152
153impl Activity {
154    /// What the day's failures mean: bad only when a runner failed, since a job's own failing steps are its
155    /// repository's.
156    pub fn failed_tone(&self) -> Tone {
157        if self.runner_failed > 0 {
158            Tone::Bad
159        } else {
160            Tone::Neutral
161        }
162    }
163}
164
165#[cfg(test)]
166mod tests;