1use super::{
6 BuildCacheCounts, BuildCacheStats, CacheKind, CacheVolume, LogSummary, RecordLimits,
7 RecordNote, RecordRemoval, Records, Tone, period,
8};
9use crate::config::ByteSize;
10
11pub const RECORDS_NEAR_FULL: u64 = 80;
14
15pub fn records_near_full(bytes: u64, max_total: u64) -> bool {
17 u128::from(bytes) * 100 >= u128::from(max_total) * u128::from(RECORDS_NEAR_FULL)
18}
19
20impl CacheKind {
21 pub fn description(&self) -> String {
24 match self {
25 CacheKind::Snapshot { profile } => format!("{profile} snapshot"),
26 CacheKind::Slot { profile, slot } => format!("{profile} slot {slot} layer"),
27 CacheKind::Pnpm => "pnpm store, shared".to_owned(),
28 CacheKind::Maven => "Maven repository, shared".to_owned(),
29 CacheKind::Mirror => "Docker Hub mirror".to_owned(),
30 CacheKind::Build => "build cache, shared".to_owned(),
31 }
32 }
33
34 pub fn profile(&self) -> Option<&str> {
36 match self {
37 CacheKind::Snapshot { profile } | CacheKind::Slot { profile, .. } => Some(profile),
38 CacheKind::Pnpm | CacheKind::Maven | CacheKind::Mirror | CacheKind::Build => None,
39 }
40 }
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct CacheLimit {
46 pub limit: u64,
48 pub over: bool,
50 pub label: String,
53}
54
55impl CacheLimit {
56 pub fn tone(&self) -> Tone {
58 if self.over { Tone::Warn } else { Tone::Neutral }
59 }
60}
61
62impl CacheVolume {
63 pub fn limit_status(&self) -> Option<CacheLimit> {
65 let limit = self.limit?;
66 let size = ByteSize(limit);
67 let label = match self.kind {
68 CacheKind::Build => format!("trimmed to {size}"),
69 CacheKind::Snapshot { .. } => format!("started over at its next reseed above {size}"),
70 CacheKind::Slot { .. } | CacheKind::Pnpm | CacheKind::Maven | CacheKind::Mirror => {
71 format!("emptied above {size}")
72 }
73 };
74 Some(CacheLimit {
75 limit,
76 over: self.bytes > limit,
77 label,
78 })
79 }
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub struct BuildCacheActivity {
85 pub counts: BuildCacheCounts,
87}
88
89impl BuildCacheCounts {
90 pub fn activity(&self) -> Option<BuildCacheActivity> {
93 (self.lookups() > 0 || self.written_bytes > 0 || self.refused > 0)
94 .then_some(BuildCacheActivity { counts: *self })
95 }
96}
97
98impl BuildCacheActivity {
99 pub fn lookups_line(&self) -> String {
101 let counts = &self.counts;
102 match counts.hit_percent() {
103 Some(percent) => format!(
104 "{} of {} lookups hit ({percent}%)",
105 counts.hits,
106 counts.lookups()
107 ),
108 None => "no lookups".to_owned(),
109 }
110 }
111
112 pub fn served_line(&self) -> String {
114 format!("{} served", ByteSize(self.counts.read_bytes))
115 }
116
117 pub fn stored_line(&self) -> String {
119 if self.counts.writer {
120 format!("{} stored", ByteSize(self.counts.written_bytes))
121 } else {
122 "read only".to_owned()
123 }
124 }
125
126 pub fn refused_line(&self) -> Option<String> {
128 (self.counts.refused > 0).then(|| format!("{} uploads refused", self.counts.refused))
129 }
130
131 pub fn line(&self) -> String {
133 let mut parts = vec![self.lookups_line(), self.served_line(), self.stored_line()];
134 parts.extend(self.refused_line());
135 parts.join(", ")
136 }
137}
138
139impl BuildCacheStats {
140 pub fn week_line(&self) -> String {
144 let week = &self.week;
145 let mut parts = match week.hit_percent() {
146 Some(percent) => vec![
147 format!("{percent}% of {} lookups hit in 7 days", week.lookups()),
148 format!("{} served", ByteSize(week.read_bytes)),
149 ],
150 None => vec!["no lookups in 7 days".to_owned()],
151 };
152 if week.activity().is_some() {
153 parts.push(format!("{} stored", ByteSize(week.written_bytes)));
154 }
155 if week.refused > 0 {
156 parts.push(format!("{} uploads refused", week.refused));
157 }
158 format!("{} entries; {}", self.entries, parts.join(", "))
159 }
160}
161
162impl RecordLimits {
163 pub fn near_full(&self, bytes: u64) -> bool {
165 records_near_full(bytes, self.max_total)
166 }
167}
168
169impl Records {
170 pub fn summary(&self) -> String {
173 match self.pressure() {
174 Some(pressure) => format!("{}; {pressure}", self.usage()),
175 None => self.usage(),
176 }
177 }
178
179 pub fn usage(&self) -> String {
181 let limits = &self.limits;
182 let age = match limits.max_age / 86_400 {
184 1 if limits.max_age == 86_400 => "1 day".to_owned(),
185 days if days > 1 && limits.max_age.is_multiple_of(86_400) => format!("{days} days"),
186 _ => period(limits.max_age),
187 };
188 format!(
189 "{} of {} records, {} of {}, each kept {age}",
190 self.records.len(),
191 limits.max_count,
192 ByteSize(self.bytes()),
193 ByteSize(limits.max_total),
194 )
195 }
196
197 pub fn pressure(&self) -> Option<&'static str> {
199 self.limits
200 .near_full(self.bytes())
201 .then_some("near max_total, so new ones remove the oldest")
202 }
203
204 pub fn summary_tone(&self) -> Tone {
206 if self.limits.near_full(self.bytes()) {
207 Tone::Warn
208 } else {
209 Tone::Neutral
210 }
211 }
212
213 pub fn empty_reason() -> &'static str {
216 "none kept: a profile with record_failures = true keeps each failed job's runner as a record, to open a \
217 shell where it failed"
218 }
219
220 pub fn shell_open(&self, job: u64) -> bool {
222 self.shells.contains(&job)
223 }
224}
225
226#[derive(Debug, Clone, Copy, PartialEq, Eq)]
228pub enum RecordState<'note> {
229 Skipped {
231 why: &'note str,
233 },
234 Removed {
236 at: u64,
238 by: Option<RecordRemoval>,
240 },
241 Kept {
243 bytes: Option<u64>,
245 },
246}
247
248impl RecordNote {
249 pub fn state(&self) -> RecordState<'_> {
251 match (&self.skipped, self.removed_at) {
252 (Some(why), _) => RecordState::Skipped { why },
253 (None, Some(at)) => RecordState::Removed {
254 at,
255 by: self.removed_by,
256 },
257 (None, None) => RecordState::Kept { bytes: self.bytes },
258 }
259 }
260
261 pub fn shell_command(job: u64) -> String {
263 format!("runnerctl shell {job}")
264 }
265}
266
267impl RecordState<'_> {
268 pub fn line(&self) -> String {
271 match self {
272 RecordState::Skipped { why } => format!("not kept: {why}"),
273 RecordState::Removed { by, .. } => {
274 by.map_or_else(|| "removed".to_owned(), |by| by.to_string())
275 }
276 RecordState::Kept { bytes: Some(bytes) } => {
277 format!("record kept, {}", ByteSize(*bytes))
278 }
279 RecordState::Kept { bytes: None } => "record kept".to_owned(),
280 }
281 }
282}
283
284impl LogSummary {
285 pub fn description(&self) -> String {
288 let line = format!("{} lines, {}", self.lines, ByteSize(self.bytes));
289 if self.truncated {
290 format!("{line}, cut at history.log_max")
291 } else {
292 line
293 }
294 }
295}
296
297#[cfg(test)]
298mod tests;