Skip to main content

runnerd_core/api/
events.rs

1//! What `GET /v1/events` streams: one enum of everything that happens, wrapped with its ID and time. Durable events
2//! are stored and get an ID, so a client resumes after them with `Last-Event-ID`; live events (samples and log
3//! lines) are only sent to connected clients, and a client that missed them reads the job's samples or log instead.
4
5use serde::{Deserialize, Serialize};
6
7use super::jobs::{HostSample, JobSummary, Sample, Step};
8use super::{Budget, ProfileStatus, RepositoryStatus, SlotStatus};
9
10/// One event as `GET /v1/events` sends it: the SSE `data` field. The SSE `event` field is `event.kind()` and the
11/// SSE `id` field is `id`.
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13pub struct EventEnvelope {
14    /// Increases with every durable event; `None` for live events.
15    pub id: Option<u64>,
16    /// When it happened.
17    pub at: u64,
18    /// What happened.
19    #[serde(flatten)]
20    pub event: Event,
21}
22
23/// Everything that happens, tagged by `type`.
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(tag = "type")]
26pub enum Event {
27    /// A job that runners of this daemon may run was queued.
28    #[serde(rename = "job.queued")]
29    JobQueued {
30        /// The job.
31        job: JobSummary,
32    },
33    /// A runner of this daemon took the job.
34    #[serde(rename = "job.started")]
35    JobStarted {
36        /// The job.
37        job: JobSummary,
38    },
39    /// A step of a running job started or completed.
40    #[serde(rename = "job.step")]
41    JobStep {
42        /// The job's ID.
43        job: u64,
44        /// The step as it is now.
45        step: Step,
46    },
47    /// The job's record is final: it completed and its runner was removed, it was lost, or it ran elsewhere.
48    #[serde(rename = "job.finished")]
49    JobFinished {
50        /// The job.
51        job: JobSummary,
52    },
53    /// A slot's runner started, took a job, or was removed.
54    #[serde(rename = "slot.changed")]
55    SlotChanged {
56        /// The profile.
57        profile: String,
58        /// The slot as it is now.
59        slot: SlotStatus,
60    },
61    /// The daemon started or a reload added the profile, a reload changed its image, CPUs, memory or slots, or it
62    /// started or stopped reseeding. A slot's runner changing is `slot.changed` only; this event carries every slot as it is now, so a
63    /// client can replace the whole profile.
64    #[serde(rename = "profile.changed")]
65    ProfileChanged {
66        /// The profile as it is now.
67        profile: ProfileStatus,
68    },
69    /// A reload removed the profile and its last runner and reseed are gone.
70    #[serde(rename = "profile.removed")]
71    ProfileRemoved {
72        /// Its name.
73        profile: String,
74    },
75    /// The memory caps in use changed.
76    #[serde(rename = "budget.changed")]
77    BudgetChanged {
78        /// The budget as it is now.
79        budget: Budget,
80    },
81    /// A repository was paused or resumed, its queue changed, a reload changed its profile or labels, or a reload
82    /// added it.
83    #[serde(rename = "repository.changed")]
84    RepositoryChanged {
85        /// The repository as it is now.
86        repository: RepositoryStatus,
87    },
88    /// A reload removed the repository from the config.
89    #[serde(rename = "repository.removed")]
90    RepositoryRemoved {
91        /// Its `owner/name`.
92        repository: String,
93    },
94    /// A profile's snapshot check finished.
95    #[serde(rename = "cache.reseeded")]
96    CacheReseeded {
97        /// The profile.
98        profile: String,
99        /// What the seed container printed, such as why it did not reseed.
100        output: String,
101        /// Set when reseeding failed.
102        error: Option<String>,
103    },
104    /// Live: a running job's container was sampled.
105    #[serde(rename = "job.sample")]
106    JobSample {
107        /// The job's ID.
108        job: u64,
109        /// The sample.
110        sample: Sample,
111    },
112    /// Live: the slice runners run in was sampled.
113    #[serde(rename = "host.sample")]
114    HostSample {
115        /// The sample.
116        sample: HostSample,
117    },
118    /// Live: a line of a running job's log was stored. Only sent with `logs=true`.
119    #[serde(rename = "log.line")]
120    LogLine {
121        /// The job's ID.
122        job: u64,
123        /// The line's number in the job's log, 0-based.
124        line: u64,
125        /// The line, without its newline.
126        text: String,
127    },
128}
129
130impl Event {
131    /// The `type` tag, also the SSE event name.
132    pub fn kind(&self) -> &'static str {
133        match self {
134            Event::JobQueued { .. } => "job.queued",
135            Event::JobStarted { .. } => "job.started",
136            Event::JobStep { .. } => "job.step",
137            Event::JobFinished { .. } => "job.finished",
138            Event::SlotChanged { .. } => "slot.changed",
139            Event::ProfileChanged { .. } => "profile.changed",
140            Event::ProfileRemoved { .. } => "profile.removed",
141            Event::BudgetChanged { .. } => "budget.changed",
142            Event::RepositoryChanged { .. } => "repository.changed",
143            Event::RepositoryRemoved { .. } => "repository.removed",
144            Event::CacheReseeded { .. } => "cache.reseeded",
145            Event::JobSample { .. } => "job.sample",
146            Event::HostSample { .. } => "host.sample",
147            Event::LogLine { .. } => "log.line",
148        }
149    }
150
151    /// Whether it is stored and can be resumed after; live events are not.
152    pub fn is_durable(&self) -> bool {
153        !matches!(
154            self,
155            Event::JobSample { .. } | Event::HostSample { .. } | Event::LogLine { .. }
156        )
157    }
158
159    /// The job it is about, if any.
160    pub fn job(&self) -> Option<u64> {
161        match self {
162            Event::JobQueued { job } | Event::JobStarted { job } | Event::JobFinished { job } => {
163                Some(job.id)
164            }
165            Event::JobStep { job, .. }
166            | Event::JobSample { job, .. }
167            | Event::LogLine { job, .. } => Some(*job),
168            Event::SlotChanged { .. }
169            | Event::ProfileChanged { .. }
170            | Event::ProfileRemoved { .. }
171            | Event::BudgetChanged { .. }
172            | Event::RepositoryChanged { .. }
173            | Event::RepositoryRemoved { .. }
174            | Event::CacheReseeded { .. }
175            | Event::HostSample { .. } => None,
176        }
177    }
178}
179
180/// `GET /v1/events` query parameters. Resume with the `Last-Event-ID` header.
181#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
182pub struct EventQuery {
183    /// Only events about this job.
184    pub job: Option<u64>,
185    /// Also send `log.line` events.
186    #[serde(default)]
187    pub logs: bool,
188}
189
190impl EventQuery {
191    /// Whether an event passes the filter.
192    pub fn matches(&self, event: &Event) -> bool {
193        (self.logs || !matches!(event, Event::LogLine { .. }))
194            && self.job.is_none_or(|job| event.job() == Some(job))
195    }
196}