1mod backoff;
2mod context;
3mod each;
4#[cfg(feature = "remote-contracts")]
5mod feeds;
6mod flight;
7mod handling;
8#[cfg(feature = "jobs")]
9mod jobs;
10mod lease;
11#[cfg(feature = "metrics")]
12mod metrics;
13mod registry;
14mod schedules;
15mod trace;
16mod wake;
17#[cfg(feature = "local-time")]
18mod wall_clock;
19
20use std::num::{NonZeroU16, NonZeroUsize};
21use std::pin::pin;
22use std::time::Duration;
23
24use sofie_core::Result;
25
26pub use context::JobContext;
27#[cfg(feature = "remote-contracts")]
28pub use feeds::Feeds;
29pub use registry::publish_heard;
30pub use schedules::{Cadence, queue_schedule};
31#[cfg(feature = "local-time")]
32pub use wall_clock::WallClock;
33
34use crate::{Aggregate, Call, Db, Entry, Messages, Run, Trigger, erase};
35pub(crate) use registry::Registry;
36
37use wake::Wake;
38
39const DEFAULT_MAX_ATTEMPTS: i64 = 10;
40const POLL_INTERVAL: Duration = Duration::from_secs(1);
42const LEASE: Duration = Duration::from_mins(5);
44const JOB_TIMEOUT: Duration = Duration::from_mins(4);
46const UNHEARD_KEPT_FOR: Duration = Duration::from_hours(1);
48const DEFAULT_IN_FLIGHT: usize = 2;
50
51pub struct Worker {
53 db: Db,
54 messages: Messages,
55 registry: Registry,
56 max_attempts: i64,
57 in_flight: usize,
58 #[cfg(feature = "local-time")]
59 local_looks: schedules::LocalLooks,
60 #[cfg(feature = "remote-contracts")]
61 feeds: Feeds,
62}
63
64impl Worker {
65 #[must_use]
66 pub fn new(db: Db) -> Self {
67 Self {
68 messages: Messages::new(db.clone()),
69 registry: Registry::default(),
70 db,
71 max_attempts: DEFAULT_MAX_ATTEMPTS,
72 in_flight: DEFAULT_IN_FLIGHT,
73 #[cfg(feature = "local-time")]
74 local_looks: schedules::LocalLooks::default(),
75 #[cfg(feature = "remote-contracts")]
76 feeds: Feeds::default(),
77 }
78 }
79
80 #[must_use]
82 pub fn on<E: crate::Event, Args: 'static>(
83 self,
84 name: &'static str,
85 job: impl Call<JobContext, Args>,
86 ) -> Self {
87 self.register(name, Trigger::On(E::NAME), erase(job))
88 }
89
90 #[must_use]
93 pub fn every<Args: 'static, J: Call<JobContext, Args>>(
94 self,
95 name: &'static str,
96 cadence: impl Into<Cadence>,
97 job: J,
98 ) -> Self {
99 self.register(name, Trigger::Every(cadence.into()), erase(job))
100 }
101
102 #[must_use]
104 pub fn each<A: Aggregate, QueryArgs: 'static, Args: 'static>(
105 self,
106 name: &'static str,
107 interval: Duration,
108 items: impl Call<JobContext, QueryArgs, Outcome = Vec<A>>,
109 action: &'static str,
110 job: impl Call<JobContext, Args>,
111 ) -> Self {
112 self.register(action, Trigger::Queued, erase(job)).register(
113 name,
114 Trigger::Every(Cadence::Interval(interval)),
115 each::each(items, action),
116 )
117 }
118
119 fn register(mut self, name: &'static str, trigger: Trigger, run: Run<JobContext>) -> Self {
120 self.registry.add(Entry { name, trigger, run });
121 self
122 }
123
124 #[cfg(feature = "contracts")]
126 #[must_use]
127 pub fn mirror<M: crate::Mirror>(self, name: &'static str) -> Self {
128 const ONCE: Duration = Duration::from_hours(100 * 365 * 24);
129 let backfill: &'static str = Box::leak(format!("{name}.backfill").into_boxed_str());
130 let copy = move |source: crate::Dep<dyn crate::Lookup<M::Source>>, page| {
131 crate::backfill_mirror::<M>(source, page, backfill)
132 };
133 let keep = |row: crate::MirrorRow<M>| async move { Ok(row) };
134 let mut worker = self.every(backfill, ONCE, copy);
135 worker.registry.add_taking_pruned(Entry {
136 name,
137 trigger: Trigger::On(<M::Changed as crate::Event>::NAME),
138 run: erase(keep),
139 });
140 if worker
141 .registry
142 .job(crate::contracts::SWEEP_FORGOTTEN)
143 .is_some()
144 {
145 return worker;
146 }
147 worker.every(
148 crate::contracts::SWEEP_FORGOTTEN,
149 Duration::from_hours(1),
150 crate::contracts::sweep_forgotten,
151 )
152 }
153
154 #[must_use]
162 pub fn max_attempts(mut self, max_attempts: NonZeroU16) -> Self {
163 self.max_attempts = i64::from(max_attempts.get());
164 self
165 }
166
167 #[must_use]
168 pub fn batch(mut self, size: NonZeroUsize) -> Self {
169 self.in_flight = size.get();
170 self
171 }
172
173 pub async fn run(self, shutdown: impl Future<Output = ()> + Send) -> Result<()> {
175 self.registry.validate()?;
176 #[cfg(feature = "remote-contracts")]
177 self.feeds.refuse_unkept(&self.db)?;
178 publish_heard(&self);
179 self.registry
180 .schedules
181 .store(&self.messages, self.db.clock().now().timestamp())
182 .await?;
183 #[cfg(feature = "metrics")]
184 crate::metrics::watch_outbox(&self.db);
185 #[cfg(feature = "remote-contracts")]
186 let (shutdown, pulling) = self.feeds.start(&self.db, shutdown);
187 let wake = &mut Wake::new(self.messages.clone());
188 let worked = flight::work(&self, Some((pin!(shutdown), wake))).await;
189 #[cfg(feature = "remote-contracts")]
190 pulling.finish().await?;
191 worked.map(drop)
192 }
193
194 pub async fn drain(&self) -> Result<usize> {
196 self.drain_reporting()
197 .await
198 .map(|drained| drained.completed)
199 }
200
201 pub async fn drain_reporting(&self) -> Result<Drained> {
202 self.registry.validate()?;
203 #[cfg(feature = "remote-contracts")]
204 self.feeds.refuse_unkept(&self.db)?;
205 publish_heard(self);
206 self.registry
207 .schedules
208 .store(&self.messages, self.db.clock().now().timestamp())
209 .await?;
210 #[cfg(feature = "remote-contracts")]
211 self.feeds.pull_all(&self.db).await?;
212 flight::work(self, None).await
213 }
214}
215
216#[derive(Debug, Default)]
217pub struct Drained {
218 pub completed: usize,
219 pub failures: Vec<Failure>,
220}
221
222#[derive(Clone, Debug, PartialEq, Eq)]
224pub struct Failure {
225 pub job: String,
226 pub error: String,
227}