sofie_db/worker/
registry.rs1use std::collections::HashMap;
2use std::sync::Arc;
3
4use sofie_core::registrations::{INTERVALS_ALLOWED, Key, Problem, validate};
5use sofie_core::{Error, Result};
6
7use super::schedules::Schedules;
8use super::trace::Work;
9use crate::heard::{Known, Listeners};
10use crate::worker::Cadence;
11use crate::worker::{JobContext, Worker};
12use crate::{Claimed, Entry, MessageKind, Trigger};
13
14fn describe(trigger: Trigger) -> String {
15 match trigger {
16 Trigger::On(event) => format!("a listener of `{event}`"),
17 Trigger::Every(_) => "a schedule".to_owned(),
18 Trigger::Queued => "a job".to_owned(),
19 }
20}
21
22fn key(entry: &Entry<JobContext>) -> Key<'static> {
23 let every = match entry.trigger {
24 Trigger::Every(Cadence::Interval(interval)) => Some(interval),
25 _ => None,
26 };
27 Key::Job {
28 name: entry.name,
29 every,
30 }
31}
32
33pub fn publish_heard(worker: &Worker) {
34 worker.db.arrivals().heard().add(&worker.registry.known());
35}
36
37#[derive(Default)]
38pub(crate) struct Registry {
39 entries: Vec<Entry<JobContext>>,
40 by_name: HashMap<&'static str, usize>,
41 pub(super) listeners: Arc<Listeners>,
42 pub(super) schedules: Schedules,
43}
44
45impl Registry {
46 pub(super) fn known(&self) -> Known {
47 Known {
48 listeners: (*self.listeners).clone(),
49 jobs: self.by_name.keys().copied().collect(),
50 }
51 }
52
53 pub(crate) fn add(&mut self, entry: Entry<JobContext>) {
54 match entry.trigger {
55 Trigger::On(event) => {
56 Arc::make_mut(&mut self.listeners)
57 .by_event
58 .entry(event)
59 .or_default()
60 .push(entry.name);
61 }
62 Trigger::Every(cadence) => self.schedules.add(entry.name, cadence),
63 Trigger::Queued => {}
64 }
65 self.by_name.insert(entry.name, self.entries.len());
66 self.entries.push(entry);
67 }
68
69 #[cfg(feature = "contracts")]
70 pub(super) fn add_taking_pruned(&mut self, entry: Entry<JobContext>) {
71 Arc::make_mut(&mut self.listeners)
72 .taking_pruned
73 .insert(entry.name);
74 self.add(entry);
75 }
76
77 pub(crate) fn validate(&self) -> Result<()> {
79 let keys: Vec<Key> = self.entries.iter().map(key).collect();
80 let problems: Vec<String> = validate(&keys)
81 .into_iter()
82 .map(|problem| self.describe(problem))
83 .chain(self.schedules.unknown_zones())
84 .collect();
85 if problems.is_empty() {
86 return Ok(());
87 }
88 Err(Error::internal_message(problems.join("; ")))
89 }
90
91 fn describe(&self, problem: Problem) -> String {
92 match problem {
93 Problem::SameJobName(first, second) => format!(
94 "job `{}` is registered twice: as {} and as {}",
95 self.entries[first].name,
96 describe(self.entries[first].trigger),
97 describe(self.entries[second].trigger)
98 ),
99 Problem::Interval(index) => {
100 let Entry { name, trigger, .. } = &self.entries[index];
101 let Trigger::Every(Cadence::Interval(interval)) = trigger else {
102 unreachable!("only a schedule has an interval")
103 };
104 format!("schedule `{name}` runs every {interval:?}; {INTERVALS_ALLOWED}")
105 }
106 Problem::SamePath(..) | Problem::SameRouteName(..) => {
107 unreachable!("a worker validates no routes")
108 }
109 }
110 }
111
112 pub(crate) fn job(&self, name: &str) -> Option<&Entry<JobContext>> {
113 self.by_name.get(name).map(|index| &self.entries[*index])
114 }
115
116 pub(super) fn only_listener(
118 &self,
119 message: &Claimed,
120 heard: &Listeners,
121 ) -> Option<&'static str> {
122 let taking = heard.taking(&message.name, &message.payload);
123 match (message.kind, taking.as_ref()) {
124 (MessageKind::Event, [only]) if self.job(only).is_some() => Some(only),
125 _ => None,
126 }
127 }
128
129 pub(super) fn work_of(&self, message: &Claimed, only_listener: Option<&str>) -> Work {
130 match message.kind {
131 MessageKind::Event if only_listener.is_some() => Work::Listener,
132 MessageKind::Event => Work::Event,
133 MessageKind::Job => match self.job(&message.name) {
134 Some(job) if matches!(job.trigger, Trigger::On(_)) => Work::Listener,
135 _ => Work::Job,
136 },
137 }
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use std::time::Duration;
144
145 use sofie_core::Result;
146
147 use super::*;
148 use crate::erase;
149
150 async fn tidy() -> Result<()> {
151 Ok(())
152 }
153
154 fn entry(name: &'static str, trigger: Trigger) -> Entry<JobContext> {
155 Entry {
156 name,
157 trigger,
158 run: erase(tidy),
159 }
160 }
161
162 fn refusal(entries: Vec<Entry<JobContext>>) -> String {
163 let mut registry = Registry::default();
164 for entry in entries {
165 registry.add(entry);
166 }
167 registry
168 .validate()
169 .expect_err("the registration is refused")
170 .to_string()
171 }
172
173 #[test]
174 fn a_second_job_of_one_name_is_refused() {
175 let refusal = refusal(vec![
176 entry("tidy", Trigger::Every(Duration::from_secs(60).into())),
177 entry("tidy", Trigger::Queued),
178 ]);
179 assert!(
180 refusal.contains("job `tidy` is registered twice: as a schedule and as a job"),
181 "{refusal}"
182 );
183 }
184
185 #[test]
186 fn a_listener_and_a_schedule_cannot_share_a_name() {
187 let refusal = refusal(vec![
188 entry("answer", Trigger::On("test.Ping")),
189 entry("answer", Trigger::Every(Duration::from_secs(60).into())),
190 ]);
191 assert!(
192 refusal.contains(
193 "job `answer` is registered twice: as a listener of `test.Ping` and as a schedule"
194 ),
195 "{refusal}"
196 );
197 }
198
199 #[test]
200 fn a_schedule_more_often_than_once_a_second_is_refused() {
201 let refusal = refusal(vec![entry(
202 "eager",
203 Trigger::Every(Duration::from_millis(10).into()),
204 )]);
205 assert!(
206 refusal.contains("schedule `eager` runs every 10ms; a schedule runs at most once"),
207 "{refusal}"
208 );
209 }
210
211 #[cfg(feature = "local-time")]
212 #[test]
213 fn a_schedule_in_an_unknown_zone_is_refused_at_start() {
214 let at = sofie_values::TimeOfDay::constant("03:00");
215 let refusal = refusal(vec![entry(
216 "nightly",
217 Trigger::Every(crate::worker::WallClock::daily(at, "Mars/Olympus").into()),
218 )]);
219 assert!(
220 refusal
221 .contains("schedule `nightly` cannot run: `Mars/Olympus` is not an IANA time zone"),
222 "{refusal}"
223 );
224 }
225
226 #[test]
227 fn distinct_jobs_are_accepted() {
228 let mut registry = Registry::default();
229 registry.add(entry(
230 "tidy",
231 Trigger::Every(Duration::from_secs(60).into()),
232 ));
233 registry.add(entry("answer", Trigger::On("test.Ping")));
234 assert!(registry.validate().is_ok());
235 assert_eq!(registry.job("answer").map(|job| job.name), Some("answer"));
236 }
237}