Skip to main content

jobby/
builder.rs

1use crate::{
2    states::{UnnamespacedJobType, UnnamespacedWorkerSpec, WorkerId},
3    store::{BackfillableJob, SubmittableJob},
4    Actions, Error, JobId, JobType, JobbyBlob, Worker,
5};
6
7use std::{
8    collections::HashMap,
9    ops::Add,
10    sync::{Arc, Mutex},
11    time::{Duration, SystemTime},
12};
13
14use futures::StreamExt;
15
16type Result<T, E = crate::Error> = anyhow::Result<T, E>;
17
18// TODO: log if dropped w/o building?
19#[derive(Debug)]
20pub struct JobBuilder {
21    // always needed
22    job_type: UnnamespacedJobType,
23    id: JobId,
24    // used for submits and backfills - can be none on a backfill
25    data: Option<JobbyBlob>,
26    // needed for backfills - if this is set, we're backfilling.
27    ready_at: Option<SystemTime>,
28    // needed for submits
29    is_submit: bool,
30    // defaults to 1
31    allowed_attempts: i64,
32    dependency: Option<JobId>,
33    dependency_type: Option<UnnamespacedJobType>,
34    expiry: Option<Duration>,
35}
36
37impl JobBuilder {
38    #[must_use]
39    pub fn new<T: JobType>(job_type: T, id: JobId) -> Self {
40        Self::new_unnamespaced(UnnamespacedJobType::from(job_type), id)
41    }
42
43    #[must_use]
44    pub(crate) fn new_unnamespaced(job_type: UnnamespacedJobType, id: JobId) -> Self {
45        Self {
46            job_type,
47            id,
48            data: None,
49            ready_at: None,
50            is_submit: false,
51            allowed_attempts: 1,
52            dependency: None,
53            dependency_type: None,
54            expiry: None,
55        }
56    }
57
58    #[must_use]
59    pub fn data(self, data: JobbyBlob) -> Self {
60        Self {
61            data: Some(data),
62            ..self
63        }
64    }
65
66    #[must_use]
67    pub fn backfill_at(self, ready_at: SystemTime) -> Self {
68        Self {
69            ready_at: Some(ready_at),
70            ..self
71        }
72    }
73
74    #[must_use]
75    pub fn backfill_in(self, ready_in: Duration) -> Self {
76        Self {
77            ready_at: Some(SystemTime::now().add(ready_in)),
78            ..self
79        }
80    }
81
82    #[must_use]
83    pub fn submit(self) -> Self {
84        Self {
85            is_submit: true,
86            ..self
87        }
88    }
89
90    #[must_use]
91    pub fn allowed_attempts(self, allowed_attempts: i64) -> Self {
92        Self {
93            allowed_attempts,
94            ..self
95        }
96    }
97
98    #[must_use]
99    pub fn dependency<T: JobType>(self, dependency_type: T, dependency: JobId) -> Self {
100        Self {
101            dependency: Some(dependency),
102            dependency_type: Some(UnnamespacedJobType::from(dependency_type)),
103            ..self
104        }
105    }
106
107    #[must_use]
108    pub(crate) fn unnamespaced_dependency(
109        self,
110        dependency_type: UnnamespacedJobType,
111        dependency: JobId,
112    ) -> Self {
113        Self {
114            dependency: Some(dependency),
115            dependency_type: Some(dependency_type),
116            ..self
117        }
118    }
119
120    #[must_use]
121    pub fn expiry(self, expiry: Option<Duration>) -> Self {
122        Self { expiry, ..self }
123    }
124
125    #[must_use]
126    pub fn without_expiry(self) -> Self {
127        Self {
128            expiry: None,
129            ..self
130        }
131    }
132
133    #[must_use]
134    pub fn expires_in(self, expires_in: Duration) -> Self {
135        Self {
136            expiry: Some(expires_in),
137            ..self
138        }
139    }
140
141    // TODO: See if we should run the validations upfront. Probably should
142    pub fn build(self, actions: &mut Actions) {
143        actions.jobs.push(self);
144    }
145
146    // TODO: See if we should run the validations upfront. Probably should
147    // TODO: See if we can name this better
148    pub fn register(self, registry: &JobRegistry) {
149        registry.register(self);
150    }
151
152    pub(crate) async fn into_jobs(
153        self,
154    ) -> Result<(Option<SubmittableJob>, Option<BackfillableJob>)> {
155        let data = if let Some(blob) = self.data {
156            Some(blob.encode().await?.into_parts())
157        } else {
158            None
159        };
160        let backfill = if let Some(ready_at) = self.ready_at {
161            Some(BackfillableJob {
162                job_type: self.job_type,
163                id: self.id.clone(),
164                data: data.clone(),
165                ready_at,
166            })
167        } else {
168            None
169        };
170        let submit = self.is_submit.then(|| {
171            let data = data.unwrap_or_else(JobbyBlob::empty_parts);
172            SubmittableJob {
173                job_type: self.job_type,
174                id: self.id.clone(),
175                data,
176                allowed_attempts: self.allowed_attempts,
177                dependency: self.dependency,
178                dependency_type: self.dependency_type,
179                expiry: self.expiry,
180            }
181        });
182        Ok((submit, backfill))
183    }
184
185    pub(crate) async fn bulk_into_jobs(
186        builders: Vec<Self>,
187    ) -> Result<(Vec<BackfillableJob>, Vec<SubmittableJob>)> {
188        if builders.is_empty() {
189            return Ok((vec![], vec![]));
190        }
191        let mut backfills: Vec<BackfillableJob> = vec![];
192        let mut submits: Vec<SubmittableJob> = vec![];
193        let futures = builders.into_iter().map(Self::into_jobs);
194        let stream = futures::stream::iter(futures).buffered(crate::FUTURES_IN_PARALLEL);
195        let maybe_jobses = stream.collect::<Vec<_>>().await;
196        let jobses = maybe_jobses.into_iter().collect::<Result<Vec<_>>>()?;
197        for (maybe_submit, maybe_backfill) in jobses {
198            if let Some(submit) = maybe_submit {
199                submits.push(submit);
200            }
201            if let Some(backfill) = maybe_backfill {
202                backfills.push(backfill);
203            }
204        }
205        Ok((backfills, submits))
206    }
207}
208
209pub struct WorkerRegistryStorage {
210    pub(crate) workers: HashMap<WorkerId, Worker>,
211    pub(crate) specs: Vec<UnnamespacedWorkerSpec>,
212}
213
214impl WorkerRegistryStorage {
215    pub(crate) fn new() -> Self {
216        Self {
217            workers: HashMap::new(),
218            specs: vec![],
219        }
220    }
221
222    fn register<T: JobType>(
223        &mut self,
224        worker: Worker,
225        id: WorkerId,
226        job_type: T,
227        fetch_interval: Duration,
228        fetch_jitter: Duration,
229        last_fetch_time: Option<SystemTime>,
230    ) -> Result<()> {
231        if self.workers.contains_key(&id) {
232            return Err(Error::WorkerAlreadyRegistered(id));
233        }
234        self.workers.insert(id.clone(), worker);
235        self.specs.push(UnnamespacedWorkerSpec {
236            id,
237            job_type: UnnamespacedJobType::from(job_type),
238            fetch_interval,
239            fetch_jitter,
240            last_fetch_time,
241        });
242        Ok(())
243    }
244}
245
246pub struct WorkerRegistry {
247    client_name: &'static str,
248    storage: Arc<Mutex<WorkerRegistryStorage>>,
249}
250
251impl WorkerRegistry {
252    pub(crate) fn new(
253        client_name: &'static str,
254        storage: Arc<Mutex<WorkerRegistryStorage>>,
255    ) -> Self {
256        Self {
257            client_name,
258            storage,
259        }
260    }
261
262    /// # Panics
263    ///
264    /// Will panic if mutex is poisoned
265    #[allow(clippy::unwrap_in_result)]
266    pub fn register<T: JobType>(
267        &self,
268        worker: Worker,
269        id: &str,
270        job_type: T,
271        fetch_interval: Duration,
272        fetch_jitter: Duration,
273        // You mostly want to set this to `None` which will set it to now + fetch_interval
274        // to avoid running a ton of jobs immediately on startup
275        last_fetch_time: Option<SystemTime>,
276    ) -> Result<()> {
277        let id = format!("{}_{id}", self.client_name);
278        self.storage.lock().expect("mutex not poisoned").register(
279            worker,
280            id,
281            job_type,
282            fetch_interval,
283            fetch_jitter,
284            last_fetch_time,
285        )
286    }
287}
288
289pub struct JobRegistry {
290    storage: Arc<Mutex<Vec<JobBuilder>>>,
291}
292
293impl JobRegistry {
294    pub(crate) fn new(storage: Arc<Mutex<Vec<JobBuilder>>>) -> Self {
295        Self { storage }
296    }
297
298    pub(crate) fn register(&self, builder: JobBuilder) {
299        self.storage
300            .lock()
301            .expect("mutex not poisoned")
302            .push(builder);
303    }
304
305    pub fn submit_job<T: JobType>(&self, job_type: T, id: JobId) -> JobBuilder {
306        JobBuilder::new(job_type, id).submit()
307    }
308
309    pub fn backfill_job_at<T: JobType>(
310        &self,
311        job_type: T,
312        id: JobId,
313        ready_at: SystemTime,
314    ) -> JobBuilder {
315        JobBuilder::new(job_type, id).backfill_at(ready_at)
316    }
317
318    pub fn backfill_job_in<T: JobType>(
319        &self,
320        job_type: T,
321        id: JobId,
322        ready_in: Duration,
323    ) -> JobBuilder {
324        JobBuilder::new(job_type, id).backfill_in(ready_in)
325    }
326}