Skip to main content

runnerd_core/api/
jobs.rs

1//! Job history as clients read it: one record per GitHub job a runner of this daemon ran, its timeline, steps,
2//! metrics and log size; the queries that list them; sample series for graphs; per-repository aggregates; cache
3//! sizes. Times are seconds since the Unix epoch, sizes are bytes, CPU is in millicores (1000 = one core busy).
4
5use serde::{Deserialize, Serialize};
6
7/// Where a job is in its life, as this daemon saw it.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(rename_all = "kebab-case")]
10pub enum JobState {
11    /// Waiting for a runner.
12    Queued,
13    /// A runner of this daemon took it.
14    Running,
15    /// GitHub reports it completed; `conclusion` says how.
16    Completed,
17    /// Its runner's container is gone and GitHub did not report the job completed in time.
18    Lost,
19    /// A runner of another machine took it, or it was cancelled before any runner did.
20    Elsewhere,
21}
22
23impl JobState {
24    /// Whether the job can still change.
25    pub fn is_final(self) -> bool {
26        matches!(
27            self,
28            JobState::Completed | JobState::Lost | JobState::Elsewhere
29        )
30    }
31}
32
33/// A job without its steps: what lists and events carry.
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35pub struct JobSummary {
36    /// GitHub's job ID, also its check run ID.
37    pub id: u64,
38    /// `owner/name` on GitHub.
39    pub repository: String,
40    /// The workflow run it belongs to.
41    pub run_id: u64,
42    /// 1 for the first attempt of the run, higher for re-runs.
43    pub run_attempt: u32,
44    /// The workflow's name.
45    pub workflow: Option<String>,
46    /// The job's name in the workflow.
47    pub name: String,
48    /// The branch the run is for.
49    pub branch: Option<String>,
50    /// The job's page on GitHub.
51    pub url: Option<String>,
52    /// The workflow's `runs-on` labels.
53    pub labels: Vec<String>,
54    /// Where the job is.
55    pub state: JobState,
56    /// GitHub's conclusion once completed: `success`, `failure`, `cancelled`, `skipped`, `timed_out`, ….
57    pub conclusion: Option<String>,
58    /// The runner that ran it.
59    pub runner: Option<JobRunner>,
60    /// When each phase started.
61    pub timeline: Timeline,
62    /// CPU, memory and network use while it ran.
63    pub metrics: JobMetrics,
64    /// How much of its log is stored.
65    pub log: LogSummary,
66}
67
68/// `GET /v1/jobs/{id}`: a job with its steps.
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
70pub struct Job {
71    /// Everything but the steps.
72    #[serde(flatten)]
73    pub summary: JobSummary,
74    /// The steps as GitHub numbers them, including `Set up job` and `Complete job`.
75    pub steps: Vec<Step>,
76    /// Its record, for a failed job of a profile that records failures.
77    #[serde(default)]
78    pub record: Option<super::RecordNote>,
79    /// How its runner was admitted; `None` for one started before runnerd recorded it.
80    #[serde(default)]
81    pub admission: Option<super::Admission>,
82}
83
84/// The runner a job ran on.
85#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
86pub struct JobRunner {
87    /// The runner's name, also its container's.
88    pub name: String,
89    /// The profile it ran in.
90    pub profile: String,
91    /// The slot it ran in, 1-based.
92    pub slot: usize,
93    /// Its container's memory cap in bytes, which a reload since may have changed in the profile. `None` in records
94    /// from before runnerd kept it.
95    pub memory_cap: Option<u64>,
96    /// Its container's CPU limit in millicores, the unit of the CPU use in `Sample` and `JobMetrics`. `None` in
97    /// records from before runnerd kept it, or when Docker reports no limit.
98    pub cpu_cap: Option<u64>,
99}
100
101/// A job's phases, in the order they happen. A runner's container starts before GitHub gives it a job.
102#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
103pub struct Timeline {
104    /// GitHub queued the job.
105    pub queued_at: u64,
106    /// The container of the runner that took it started.
107    pub container_started_at: Option<u64>,
108    /// The runner started the job.
109    pub started_at: Option<u64>,
110    /// The job finished, by GitHub's clock; or when its container was found gone.
111    pub finished_at: Option<u64>,
112    /// The container and the runner's registration were removed.
113    pub cleaned_at: Option<u64>,
114}
115
116/// What a job used, over the samples taken while it ran (every few seconds). `None` until a sample exists.
117#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
118pub struct JobMetrics {
119    /// CPU time, in milliseconds.
120    pub cpu_time: Option<u64>,
121    /// The highest CPU use of a sample interval, in millicores.
122    pub cpu_peak: Option<u64>,
123    /// CPU time over wall time, in millicores.
124    pub cpu_average: Option<u64>,
125    /// The highest memory use without page cache.
126    pub memory_peak: Option<u64>,
127    /// The average memory use without page cache.
128    pub memory_average: Option<u64>,
129    /// Bytes the container received.
130    pub network_received: Option<u64>,
131    /// Bytes the container sent.
132    pub network_sent: Option<u64>,
133    /// Of the bytes received, those the Docker Hub mirror served.
134    pub mirror_bytes: u64,
135}
136
137/// A job's stored log.
138#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
139pub struct LogSummary {
140    /// Lines stored.
141    pub lines: u64,
142    /// Bytes stored.
143    pub bytes: u64,
144    /// Set once the log reached `history.log_max` and later lines were dropped.
145    pub truncated: bool,
146}
147
148/// One step of a job, as GitHub reports it.
149#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
150pub struct Step {
151    /// GitHub's step number; numbers may skip.
152    pub number: u32,
153    /// The name GitHub shows, such as `Set up job` or `Run docker run --rm hello-world`.
154    pub name: String,
155    /// `pending`, `queued`, `in_progress` or `completed`.
156    pub status: String,
157    /// Once completed: `success`, `failure`, `skipped`, `cancelled`, ….
158    pub conclusion: Option<String>,
159    /// When it started.
160    pub started_at: Option<u64>,
161    /// When it completed.
162    pub completed_at: Option<u64>,
163}
164
165/// `GET /v1/jobs` query parameters. Jobs come newest first (by ID, which grows with time).
166#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
167pub struct JobQuery {
168    /// Only this `owner/name`.
169    pub repository: Option<String>,
170    /// Only jobs in this state.
171    pub state: Option<JobState>,
172    /// Only jobs with this conclusion, such as `failure`.
173    pub conclusion: Option<String>,
174    /// Only jobs queued at or after this time.
175    pub since: Option<u64>,
176    /// Only jobs queued before this time.
177    pub until: Option<u64>,
178    /// Only jobs with a smaller ID: the `next` of the previous page.
179    pub before: Option<u64>,
180    /// At most this many jobs; default 50, at most 500.
181    pub limit: Option<u32>,
182}
183
184/// `GET /v1/jobs`: one page of jobs.
185#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
186pub struct JobPage {
187    /// Newest first.
188    pub jobs: Vec<JobSummary>,
189    /// Pass as `before` for the next page; `None` on the last page.
190    pub next: Option<u64>,
191}
192
193/// `GET /v1/jobs/{id}/logs` query parameters. The response is `text/plain`, one log line per line.
194#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
195pub struct LogQuery {
196    /// Start at this line, 0-based.
197    pub from: Option<u64>,
198    /// Keep the response open and send lines as they are stored, until the job finishes.
199    #[serde(default)]
200    pub follow: bool,
201}
202
203/// One sample of a container's use.
204#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
205pub struct Sample {
206    /// When it was taken.
207    pub at: u64,
208    /// CPU use since the previous sample, in millicores.
209    pub cpu: u64,
210    /// Memory use without page cache.
211    pub memory: u64,
212    /// Bytes received since the container started.
213    pub network_received: u64,
214    /// Bytes sent since the container started.
215    pub network_sent: u64,
216}
217
218/// `GET /v1/jobs/{id}/samples`: what the job's container used over time, from its start to its removal. Samples
219/// older than `history.samples` are deleted; the job's metrics stay.
220#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
221pub struct SampleSeries {
222    /// The job.
223    pub job: u64,
224    /// Seconds between samples.
225    pub interval: u64,
226    /// Oldest first.
227    pub samples: Vec<Sample>,
228}
229
230/// One sample of the whole slice runners run in.
231#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
232pub struct HostSample {
233    /// When it was taken.
234    pub at: u64,
235    /// CPU use of the slice since the previous sample, in millicores.
236    pub cpu: u64,
237    /// Memory use of the slice less its inactive page cache, as `docker stats` counts it: a conservative number,
238    /// since active page cache still counts, which the high-water mark of measured admission compares to
239    /// `MemoryMax`.
240    pub memory: u64,
241}
242
243/// `GET /v1/host/samples` query parameters.
244#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
245pub struct HostSampleQuery {
246    /// Only samples taken at or after this time; default the last hour, at most the last 24 hours.
247    pub since: Option<u64>,
248}
249
250/// `GET /v1/host/samples`: the slice's use over time, oldest first.
251#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
252pub struct HostSamples {
253    /// Seconds between samples.
254    pub interval: u64,
255    /// Oldest first.
256    pub samples: Vec<HostSample>,
257}
258
259/// How `GET /v1/stats` groups jobs in time.
260#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
261#[serde(rename_all = "kebab-case")]
262pub enum Bucket {
263    /// By UTC hour.
264    Hour,
265    /// By UTC day.
266    #[default]
267    Day,
268}
269
270/// `GET /v1/stats` query parameters.
271#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
272pub struct StatsQuery {
273    /// Only this `owner/name`.
274    pub repository: Option<String>,
275    /// Only jobs queued at or after this time; default 30 days ago. At most 744 buckets before `until` (or now)
276    /// are counted: 31 days of hours, two years of days.
277    pub since: Option<u64>,
278    /// Only jobs queued before this time.
279    pub until: Option<u64>,
280    /// The bucket size; default a day.
281    #[serde(default)]
282    pub bucket: Bucket,
283}
284
285/// `GET /v1/stats`: finished jobs per repository and time bucket, for trends. Sums, so clients can average.
286#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
287pub struct Stats {
288    /// The bucket size.
289    pub bucket: Bucket,
290    /// By bucket start, then repository.
291    pub rows: Vec<StatsRow>,
292}
293
294/// The finished jobs of one repository in one bucket.
295#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
296pub struct StatsRow {
297    /// The bucket's start.
298    pub start: u64,
299    /// `owner/name`.
300    pub repository: String,
301    /// Jobs completed or lost.
302    pub jobs: u64,
303    /// Of those, concluded `success`.
304    pub succeeded: u64,
305    /// Of those, concluded `failure` or `timed_out`, or lost.
306    pub failed: u64,
307    /// Of those, concluded `cancelled`.
308    pub cancelled: u64,
309    /// Seconds from queued to started, summed.
310    pub queue_seconds: u64,
311    /// Seconds from started to finished, summed.
312    pub run_seconds: u64,
313    /// CPU time, in milliseconds, summed.
314    pub cpu_time: u64,
315    /// The highest memory peak of a job.
316    pub memory_peak: u64,
317    /// Bytes received, summed.
318    pub network_received: u64,
319    /// Bytes from the mirror, summed.
320    pub mirror_bytes: u64,
321}
322
323/// What a cache volume holds.
324#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
325#[serde(rename_all = "kebab-case", tag = "kind")]
326pub enum CacheKind {
327    /// A profile's read-only snapshot.
328    Snapshot {
329        /// The profile.
330        profile: String,
331    },
332    /// A slot's layer changes and Docker Engine data.
333    Slot {
334        /// The profile.
335        profile: String,
336        /// 1-based.
337        slot: usize,
338    },
339    /// The pnpm store all slots share.
340    Pnpm,
341    /// The Maven repository all slots share.
342    Maven,
343    /// The Docker Hub mirror's storage.
344    Mirror,
345}
346
347/// One cache volume and its size.
348#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
349pub struct CacheVolume {
350    /// The Docker volume.
351    pub name: String,
352    /// What it holds.
353    #[serde(flatten)]
354    pub kind: CacheKind,
355    /// Its size at `measured_at`.
356    pub bytes: u64,
357    /// The size above which runnerd empties it, or for a snapshot starts it over at its next reseed, if it has one.
358    pub limit: Option<u64>,
359}
360
361/// `GET /v1/caches`: every cache volume of the configured profiles, as last measured. Docker measures volumes
362/// slowly, so runnerd measures them at start and whenever no runner runs after jobs ended.
363#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
364pub struct Caches {
365    /// When the sizes were measured; `None` before the first measurement.
366    pub measured_at: Option<u64>,
367    /// By name.
368    pub volumes: Vec<CacheVolume>,
369}
370
371/// `GET /v1/caches/history` query parameters.
372#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
373pub struct CacheHistoryQuery {
374    /// Only measurements at or after this time; default 30 days ago.
375    pub since: Option<u64>,
376}
377
378/// One volume's size at one measurement.
379#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
380pub struct CacheSize {
381    /// When it was measured.
382    pub at: u64,
383    /// The Docker volume.
384    pub name: String,
385    /// Its size.
386    pub bytes: u64,
387}
388
389/// `GET /v1/caches/history`: every measurement, oldest first.
390#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
391pub struct CacheHistory {
392    /// Oldest first.
393    pub sizes: Vec<CacheSize>,
394}