Skip to main content

runnerd_core/api/
budget.rs

1//! What the memory budget means as every client shows it: its line, what is reserved and free of it, and the parts
2//! a bar draws. Plain derivations of [`Status`]; clients only lay them out.
3
4use super::{AdmissionPolicy, Budget, HostMemory, Status};
5use crate::config::ByteSize;
6
7/// A figure and the words that follow it, such as `8G` and `of 24G reserved in runnerd.slice`, for a client that
8/// sets the figure apart.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct Reading {
11    /// Such as `8G`.
12    pub figure: String,
13    /// What follows it, such as `of 24G reserved in runnerd.slice`.
14    pub text: String,
15}
16
17impl Reading {
18    /// The figure and its words as one line.
19    pub fn line(&self) -> String {
20        format!("{} {}", self.figure, self.text)
21    }
22}
23
24impl Budget {
25    /// What is reserved of what, as every client shows it: `8G` `of 24G reserved in runnerd.slice`, or without
26    /// `MemoryMax` `8G` `reserved in runnerd.slice, which has no MemoryMax`; how runners are admitted
27    /// (`Budget::admission_note`) follows the slice.
28    pub fn memory_reading(&self) -> Reading {
29        let of = self
30            .memory_max
31            .map_or_else(String::new, |max| format!("of {} ", ByteSize(max)));
32        let mut text = format!("{of}reserved in {}", self.slice);
33        if let Some(note) = self.admission_note() {
34            text = format!("{text} {note}");
35        }
36        if self.memory_max.is_none() {
37            text.push_str(", which has no MemoryMax");
38        }
39        Reading {
40            figure: ByteSize(self.memory_in_use).to_string(),
41            text,
42        }
43    }
44
45    /// The budget's clauses in the order clients show them: memory (`Budget::memory_reading`), then CPU
46    /// (`CpuBudget::line`) and the host (`HostBudget::line`) when the daemon reports them.
47    pub fn clauses(&self) -> Vec<String> {
48        let mut clauses = vec![self.memory_reading().line()];
49        clauses.extend(self.cpu.map(|cpu| cpu.line()));
50        clauses.extend(self.host.as_ref().map(super::HostBudget::line));
51        clauses
52    }
53
54    /// The budget as one line, its clauses joined by `; `.
55    pub fn line(&self) -> String {
56        self.clauses().join("; ")
57    }
58
59    /// What `MemoryMax` leaves beside every running container's reservation, in bytes: what a new runner must fit
60    /// in. `None` without `MemoryMax`.
61    pub fn remaining(&self) -> Option<u64> {
62        self.memory_max
63            .map(|max| max.saturating_sub(self.memory_in_use))
64    }
65}
66
67impl HostMemory {
68    /// What the host has available of its memory, as the budget's host clause and doctor say it: `31G of 64G
69    /// available`.
70    pub fn available_line(&self) -> String {
71        format!(
72            "{} of {} available",
73            ByteSize(self.available),
74            ByteSize(self.total)
75        )
76    }
77}
78
79impl Status {
80    /// The budget as a bar draws it: a block per slot as large as its cap (under measured admission, a block per
81    /// runner as large as what it reserves), the mirror, seed containers and shells, then what is free; and the
82    /// whole the blocks are scaled against.
83    pub fn budget_parts(&self) -> BudgetParts {
84        let budget = &self.budget;
85        let slots: Vec<SlotPart> = self
86            .profiles
87            .iter()
88            .enumerate()
89            .flat_map(|(profile, status)| {
90                status.slots.iter().filter_map(move |slot| {
91                    let memory = match budget.admission {
92                        AdmissionPolicy::Caps => match &slot.runner {
93                            Some(runner) if runner.reserved > 0 => runner.reserved,
94                            _ => status.memory,
95                        },
96                        AdmissionPolicy::Measured => slot.runner.as_ref()?.reserved,
97                    };
98                    Some(SlotPart {
99                        profile,
100                        slot: slot.slot,
101                        memory,
102                        reserved: slot.runner.is_some(),
103                    })
104                })
105            })
106            .collect();
107        let mirror = (budget.mirror_memory > 0).then_some(HeldPart {
108            kind: HeldKind::Mirror,
109            memory: budget.mirror_memory,
110        });
111        let seeds = budget.seed_memory.iter().map(|seed| HeldPart {
112            kind: HeldKind::Seed {
113                profile: seed.profile.clone(),
114            },
115            memory: seed.memory,
116        });
117        let shells = budget.shell_memory.iter().map(|shell| HeldPart {
118            kind: HeldKind::Shell { job: shell.job },
119            memory: shell.memory,
120        });
121        let held: Vec<HeldPart> = mirror.into_iter().chain(seeds).chain(shells).collect();
122        let drawn: u64 = slots
123            .iter()
124            .map(|part| part.memory)
125            .chain(held.iter().map(|part| part.memory))
126            .sum();
127        let whole = budget.memory_max.unwrap_or(drawn).max(drawn);
128        let unclaimed: u64 = slots
129            .iter()
130            .filter(|part| !part.reserved)
131            .map(|part| part.memory)
132            .sum();
133        let free = budget.memory_max.and_then(|max| {
134            let memory = max.saturating_sub(drawn);
135            let text = if unclaimed == 0 {
136                format!("of {} free", ByteSize(max))
137            } else {
138                let remaining = budget.remaining().unwrap_or(0);
139                format!(
140                    "of {} free past every slot's cap; {} is free beside what runs",
141                    ByteSize(max),
142                    ByteSize(remaining)
143                )
144            };
145            (memory > 0).then(|| FreePart {
146                memory,
147                reading: Reading {
148                    figure: ByteSize(memory).to_string(),
149                    text,
150                },
151            })
152        });
153        BudgetParts {
154            slots,
155            held,
156            free,
157            whole,
158        }
159    }
160}
161
162/// The budget as a bar draws it, from `Status::budget_parts`.
163#[derive(Debug, Clone, PartialEq, Eq)]
164pub struct BudgetParts {
165    /// A block per slot, in config order: every slot under caps, only those with a runner under measured admission.
166    pub slots: Vec<SlotPart>,
167    /// The mirror, each seed container, then each shell open in a record.
168    pub held: Vec<HeldPart>,
169    /// What `MemoryMax` leaves past every block; `None` without `MemoryMax` or room past them.
170    pub free: Option<FreePart>,
171    /// What the blocks are scaled against: `MemoryMax`, or the blocks' sum when larger or without it.
172    pub whole: u64,
173}
174
175/// A slot's block of the budget.
176#[derive(Debug, Clone, Copy, PartialEq, Eq)]
177pub struct SlotPart {
178    /// Its profile's index in `Status::profiles`.
179    pub profile: usize,
180    /// The slot, 1-based.
181    pub slot: usize,
182    /// Its size in bytes: what its runner reserves, or a free slot's cap.
183    pub memory: u64,
184    /// Whether a runner reserves it; a free slot's block under caps is room its next runner takes, still free.
185    pub reserved: bool,
186}
187
188/// Memory the budget holds for something other than a slot.
189#[derive(Debug, Clone, PartialEq, Eq)]
190pub struct HeldPart {
191    /// What holds it.
192    pub kind: HeldKind,
193    /// Its cap in bytes.
194    pub memory: u64,
195}
196
197impl HeldPart {
198    /// Its name under its block: `mirror`, `node seed` or `job 42 shell`.
199    pub fn name(&self) -> String {
200        match &self.kind {
201            HeldKind::Mirror => "mirror".to_owned(),
202            HeldKind::Seed { profile } => format!("{profile} seed"),
203            HeldKind::Shell { job } => format!("job {job} shell"),
204        }
205    }
206
207    /// What holds it, after its size: `held by the mirror`, `held by node's seed container` or `held by the shell
208    /// in job 42's record`.
209    pub fn holder(&self) -> String {
210        match &self.kind {
211            HeldKind::Mirror => "held by the mirror".to_owned(),
212            HeldKind::Seed { profile } => format!("held by {profile}'s seed container"),
213            HeldKind::Shell { job } => format!("held by the shell in job {job}'s record"),
214        }
215    }
216}
217
218/// What holds a part of the budget beside the slots.
219#[derive(Debug, Clone, PartialEq, Eq)]
220pub enum HeldKind {
221    /// The registry mirror.
222    Mirror,
223    /// A profile's seed container, while it reseeds.
224    Seed {
225        /// The profile it reseeds.
226        profile: String,
227    },
228    /// A shell open in a job's record.
229    Shell {
230        /// The job.
231        job: u64,
232    },
233}
234
235/// What the bar leaves free past every block.
236#[derive(Debug, Clone, PartialEq, Eq)]
237pub struct FreePart {
238    /// In bytes.
239    pub memory: u64,
240    /// Its figure and what it means, such as `8G` `of 24G free`.
241    pub reading: Reading,
242}
243
244#[cfg(test)]
245mod tests;