apalis_core/backend/ext/poll_strategy/
mod.rs1use std::{
22 task::{Context, Poll},
23 time::{Duration, Instant},
24};
25
26mod strategies;
27pub use strategies::*;
28mod builder;
29pub use builder::*;
30mod poll_with;
31pub use poll_with::*;
32
33pub type BoxedPollStrategy = Box<dyn PollStrategy + Send + Sync + 'static>;
35
36pub trait PollStrategy {
39 fn poll_drive(&mut self, cx: &mut Context<'_>, snapshot: &PollSnapshot) -> Poll<Option<()>>;
47
48 #[allow(unused_variables)]
50 fn on_poll(&mut self, snapshot: &PollSnapshot) {}
51}
52
53#[derive(Debug, Clone, Copy, Default)]
57pub struct PollSnapshot {
58 pub consecutive_ready: usize,
60
61 pub total_ready: u64,
63
64 pub total_pending: u64,
66
67 pub idle_for: Duration,
69}
70
71#[derive(Debug, Default, Clone)]
73pub struct PollMetrics {
74 consecutive_ready: usize,
75 total_ready: u64,
76 total_pending: u64,
77 last_ready: Option<Instant>,
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
82pub enum ActivityState {
83 #[default]
85 Idle,
86 WakingUp,
88 Saturated,
90}
91
92impl ActivityState {
93 #[must_use]
94 const fn from_consecutive_ready(consecutive_ready: usize) -> Self {
95 match consecutive_ready {
96 0 => Self::Idle,
97 1 => Self::WakingUp,
98 _ => Self::Saturated,
99 }
100 }
101}
102
103impl PollSnapshot {
104 #[must_use]
106 pub const fn new() -> Self {
107 Self {
108 consecutive_ready: 0,
109 total_ready: 0,
110 total_pending: 0,
111 idle_for: Duration::from_secs(0),
112 }
113 }
114
115 #[must_use]
117 pub const fn total_polls(&self) -> u64 {
118 self.total_ready + self.total_pending
119 }
120
121 #[must_use]
123 pub const fn is_empty(&self) -> bool {
124 self.total_polls() == 0
125 }
126
127 #[must_use]
131 pub fn hit_rate(&self) -> f64 {
132 let total = self.total_polls();
133
134 if total == 0 {
135 1.0
136 } else {
137 self.total_ready as f64 / total as f64
138 }
139 }
140
141 #[must_use]
143 pub fn miss_rate(&self) -> f64 {
144 1.0 - self.hit_rate()
145 }
146
147 #[must_use]
149 pub fn idle_for_at_least(&self, duration: Duration) -> bool {
150 self.idle_for >= duration
151 }
152
153 #[must_use]
155 pub fn idle_for_less_than(&self, duration: Duration) -> bool {
156 self.idle_for < duration
157 }
158
159 #[must_use]
162 pub const fn should_poll_aggressively(&self, threshold: usize) -> bool {
163 self.consecutive_ready >= threshold
164 }
165
166 #[must_use]
169 pub fn should_backoff(&self, max_idle: Duration) -> bool {
170 self.consecutive_ready == 0 && self.idle_for >= max_idle
171 }
172
173 #[must_use]
175 pub const fn activity(&self) -> ActivityState {
176 ActivityState::from_consecutive_ready(self.consecutive_ready)
177 }
178
179 #[must_use]
181 pub const fn is_busy(&self) -> bool {
182 !matches!(self.activity(), ActivityState::Idle)
183 }
184
185 #[must_use]
187 pub const fn is_idle(&self) -> bool {
188 matches!(self.activity(), ActivityState::Idle)
189 }
190
191 #[must_use]
193 pub const fn is_waking_up(&self) -> bool {
194 matches!(self.activity(), ActivityState::WakingUp)
195 }
196
197 #[must_use]
199 pub const fn is_saturated(&self) -> bool {
200 matches!(self.activity(), ActivityState::Saturated)
201 }
202}
203
204impl PollMetrics {
205 pub fn on_ready(&mut self) {
207 self.consecutive_ready = self.consecutive_ready.saturating_add(1);
208 self.total_ready = self.total_ready.saturating_add(1);
209 self.last_ready = Some(Instant::now());
210 }
211
212 pub fn on_pending(&mut self) {
214 self.consecutive_ready = 0;
215 self.total_pending = self.total_pending.saturating_add(1);
216 }
217
218 #[must_use]
220 pub fn snapshot(&self) -> PollSnapshot {
221 PollSnapshot {
222 consecutive_ready: self.consecutive_ready,
223 total_ready: self.total_ready,
224 total_pending: self.total_pending,
225 idle_for: self
226 .last_ready
227 .map(|instant| instant.elapsed())
228 .unwrap_or_default(),
229 }
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use std::{collections::VecDeque, sync::Arc, time::Duration};
236
237 use futures_channel::mpsc;
238
239 use futures_util::{
240 FutureExt, SinkExt, StreamExt,
241 lock::Mutex,
242 sink,
243 stream::{self},
244 };
245
246 use crate::{
247 error::BoxDynError,
248 task::{Task, task_id::RandomId},
249 worker::{
250 builder::WorkerBuilder, context::WorkerContext, ext::event_listener::EventListenerExt,
251 },
252 };
253
254 use super::*;
255 use crate::backend::TaskSink;
256
257 const ITEMS: u32 = 10;
258
259 #[tokio::test]
260 #[cfg(feature = "sleep")]
261 async fn basic_strategy_backend() {
262 use crate::backend::{dequeue::VecDequeBackend, ext::BackendExt};
263 let config = BackoffConfig::new(Duration::from_secs(5))
264 .with_multiplier(1.5)
265 .with_jitter(0.2);
266
267 let interval = Duration::from_millis(200);
268 let mut backend = VecDequeBackend::new().poll_with_backoff(interval, config);
269
270 for i in 0..ITEMS {
271 backend.push(i).await.unwrap();
272 }
273
274 async fn task(task: u32, worker: WorkerContext) -> Result<(), BoxDynError> {
275 tokio::time::sleep(Duration::from_secs(1)).await;
276 if task == ITEMS - 1 {
277 tokio::time::sleep(Duration::from_secs(5)).await;
278 worker.stop().unwrap();
279 return Err("Worker stopped!")?;
280 }
281 Ok(())
282 }
283
284 let worker = WorkerBuilder::new("rango-tango")
285 .backend(backend)
286 .on_event(|worker, ev| {
287 println!("On Event = {ev:?} from {}", worker.name());
288 })
289 .build(task);
290 worker.run().await.unwrap();
291 }
292
293 #[tokio::test]
294 #[cfg(feature = "sleep")]
295 async fn custom_strategy_backend() {
296 use crate::backend::{custom::BackendBuilder, ext::BackendExt};
297
298 let memory = Arc::new(Mutex::new(VecDeque::new()));
299 let backoff = BackoffConfig::new(Duration::from_secs(5))
300 .with_multiplier(1.5)
301 .with_jitter(0.2);
302 let interval = IntervalStrategy::new(Duration::from_millis(200)).with_backoff(backoff);
303
304 let when_i_am_ready = FutureStrategy::new(move |_| {
305 crate::timer::sleep(Duration::from_millis(1500))
307 });
308
309 let (mut tx, rx) = mpsc::channel(1);
310
311 tokio::spawn(async move {
312 for i in 0..ITEMS {
313 tokio::time::sleep(Duration::from_secs((i) as u64)).await;
314 if tx.send(()).await.is_err() {
315 break;
316 }
317 }
318 });
319
320 let strategy = Strategy::new()
321 .apply(when_i_am_ready)
322 .apply(interval)
323 .apply(StreamStrategy::new(rx));
324
325 let mut backend = BackendBuilder::new_with_cfg(())
326 .database(memory)
327 .fetcher(|db, _, worker| {
328 stream::unfold((db.clone(), worker.clone()), |(p, worker)| async move {
329 let mut db = p.lock().await;
330 let item = db.pop_front();
331 drop(db);
332 if let Some(item) = item {
333 Some((Ok::<_, BoxDynError>(Some(item)), (p, worker)))
334 } else {
335 Some((Ok::<Option<Task<u32>>, BoxDynError>(None), (p, worker)))
336 }
337 })
338 .boxed()
339 })
340 .sink(|db, _| {
341 sink::unfold(db.clone(), move |p, item: Task<u32>| {
342 async move {
343 use crate::task::task_id::TaskId;
344
345 let mut db = p.lock().await;
346
347 db.push_back(
348 item.into_builder()
349 .task_id(TaskId::from_string(RandomId::default()))
350 .build(),
351 );
352 drop(db);
353 Ok::<_, BoxDynError>(p)
354 }
355 .boxed()
356 })
357 })
358 .build()
359 .unwrap()
360 .poll_with_strategy(strategy);
361
362 for i in 0..ITEMS {
363 backend.push(i).await.unwrap();
364 }
365
366 async fn task(task: u32, worker: WorkerContext) -> Result<(), BoxDynError> {
367 tokio::time::sleep(Duration::from_secs(1)).await;
368 if task == ITEMS - 1 {
369 tokio::time::sleep(Duration::from_secs(10)).await;
370 worker.stop().unwrap();
371 return Err("Worker stopped!")?;
372 }
373 Ok(())
374 }
375
376 let worker = WorkerBuilder::new("rango-tango")
377 .backend(backend)
378 .on_event(|worker, ev| {
379 println!("On Event = {ev:?} from {}", worker.name());
380 })
381 .build(task);
382 worker.run().await.unwrap();
383 }
384}