Skip to main content

apalis_core/backend/persistence/
mod.rs

1use std::{
2    collections::VecDeque,
3    fmt::Debug,
4    ops::{Deref, DerefMut},
5    pin::Pin,
6    task::{Context, Poll},
7    time::Duration,
8};
9
10use futures_channel::mpsc::{self, Receiver};
11use futures_timer::Delay;
12use futures_util::FutureExt;
13
14pub use crate::backend::persistence::{event::TaskEvent, service::TaskPersistLayer, state::State};
15use crate::{backend::future::BoxSyncFuture, task::Task, worker::context::WorkerContext};
16
17mod event;
18mod service;
19mod sink;
20mod state;
21
22/// Persistence layer for registering workers, maintaining their liveness,
23/// fetching work, recovering abandoned tasks, and handling task events.
24pub trait Persistence: Clone + Send + 'static {
25    /// The compact type the upstream backend will use
26    type Compact;
27
28    /// The response type to be stored in the db
29    type Response;
30
31    /// Error returned by persistence operations.
32    type Error: std::error::Error;
33
34    /// Registers the worker with the persistence backend.
35    fn register(
36        &mut self,
37        worker: &WorkerContext,
38    ) -> impl Future<Output = Result<(), Self::Error>> + Send;
39
40    /// Records a heartbeat for the worker.
41    fn heartbeat(
42        &mut self,
43        worker: &WorkerContext,
44    ) -> impl Future<Output = Result<(), Self::Error>> + Send;
45
46    /// Fetches the next batch of tasks available for processing.
47    fn fetch_next(
48        &mut self,
49        worker: &WorkerContext,
50    ) -> impl Future<Output = Result<Vec<Task<Self::Compact>>, Self::Error>> + Send;
51
52    /// Re-enqueues tasks that were abandoned by a worker.
53    fn reenqueue_abandoned(
54        &mut self,
55        tasks: Vec<Task<Self::Compact>>,
56        worker: &WorkerContext,
57    ) -> impl Future<Output = Result<u64, Self::Error>> + Send;
58
59    /// Handles task lifecycle events produced by the worker.
60    fn handle_events(
61        &mut self,
62        events: Vec<TaskEvent<Self::Response>>,
63        worker: &WorkerContext,
64    ) -> impl Future<Output = Result<(), Self::Error>> + Send;
65
66    /// Handle tasks being pushed
67    fn push_tasks(
68        &mut self,
69        tasks: Vec<Task<Self::Compact>>,
70    ) -> impl Future<Output = Result<(), Self::Error>> + Send;
71}
72
73/// A backend that drives a [`Persistence`] instance to emit tasks
74///
75/// This provides a lightweight way to compose a backend without worrying about polling
76#[derive(Debug)]
77pub struct Persisted<P>
78where
79    P: Persistence,
80{
81    inner: P,
82    state: State<Task<P::Compact>, P::Error>,
83    heartbeat_timer: Option<Delay>,
84    receiver: Option<Receiver<TaskEvent<P::Response>>>,
85    sink_buffer: Vec<Task<P::Compact>>,
86    sink_future: Option<BoxSyncFuture<Result<(), P::Error>>>,
87}
88
89impl<P: Clone> Clone for Persisted<P>
90where
91    P: Persistence,
92{
93    fn clone(&self) -> Self {
94        Self {
95            inner: self.inner.clone(),
96            state: State::NotBootstrapped,
97            heartbeat_timer: None,
98            receiver: None,
99            sink_buffer: Vec::new(),
100            sink_future: None,
101        }
102    }
103}
104
105impl<P> Deref for Persisted<P>
106where
107    P: Persistence,
108{
109    type Target = P;
110
111    fn deref(&self) -> &Self::Target {
112        &self.inner
113    }
114}
115
116impl<P> DerefMut for Persisted<P>
117where
118    P: Persistence,
119{
120    fn deref_mut(&mut self) -> &mut Self::Target {
121        &mut self.inner
122    }
123}
124
125macro_rules! poll_fut_or_return {
126    ($fut:expr, $cx:expr) => {
127        match $fut.poll_unpin($cx) {
128            Poll::Ready(Ok(v)) => v,
129            Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
130            Poll::Pending => return Poll::Pending,
131        }
132    };
133}
134
135impl<P> Persisted<P>
136where
137    P: Persistence + Clone,
138    P::Compact: Send + 'static,
139    P::Response: Send,
140{
141    /// Creates a new `Persisted` in the `NotBootstrapped` state.
142    ///
143    /// The worker will register itself with the backend on the first call
144    /// to [`poll_ready`](Self::poll_ready).
145    pub fn new(inner: P) -> Self {
146        Self {
147            inner,
148            heartbeat_timer: None,
149            receiver: None,
150            state: State::NotBootstrapped,
151            sink_buffer: Vec::new(),
152            sink_future: None,
153        }
154    }
155
156    /// Get the current state
157    pub fn state(&self) -> &State<Task<P::Compact>, P::Error> {
158        &self.state
159    }
160
161    /// Get the a mutable state
162    pub fn state_mut(&mut self) -> &mut State<Task<P::Compact>, P::Error> {
163        &mut self.state
164    }
165
166    /// Drains any pending events from the receiver.
167    ///
168    /// Returns `None` if there's nothing to process.
169    fn try_drain_events(&mut self, _: &mut Context<'_>) -> Option<Vec<TaskEvent<P::Response>>> {
170        let receiver = self.receiver.as_mut()?;
171        let mut events = Vec::new();
172        let mut polls = 0u32;
173
174        while let Ok(event) = receiver.try_recv() {
175            polls += 1;
176            events.push(event);
177        }
178
179        trace!(
180            drained = events.len(),
181            polls, "try_drain_events: receiver drain complete"
182        );
183
184        if events.is_empty() {
185            None
186        } else {
187            debug!(
188                count = events.len(),
189                "try_drain_events: buffered events ready"
190            );
191            Some(events)
192        }
193    }
194
195    /// Build a layer that wraps a TaskPersistLayer
196    pub fn layer<C>(&mut self, codec: C, batch_size: usize) -> TaskPersistLayer<C, P::Response> {
197        // Currently we only emit Lock and Ack
198        let buffer = batch_size * 4;
199        let (tx, rx) = mpsc::channel(buffer);
200        self.receiver = Some(rx);
201        TaskPersistLayer::new(tx, codec)
202    }
203
204    /// Drives the provider's internal state machine (bootstrap, heartbeat,
205    /// event processing) until it is genuinely ready for [`poll_next`](Self::poll_next)
206    /// to be called, or until it must return `Pending`.
207    pub fn poll_ready(
208        &mut self,
209        cx: &mut Context<'_>,
210        worker: &WorkerContext,
211        heartbeat_interval: Duration,
212    ) -> Poll<Result<(), P::Error>> {
213        trace!("poll_ready: {:?}", self.state);
214        loop {
215            match &mut self.state {
216                State::NotBootstrapped => {
217                    debug!("poll_ready: NotBootstrapped -> registering worker");
218                    let mut provider = self.inner.clone();
219                    let worker = worker.clone();
220                    let fut = async move {
221                        provider.register(&worker).await?;
222                        Ok(())
223                    };
224                    self.state = State::RegisterWorker(fut.boxed().into());
225                }
226
227                State::RegisterWorker(fut) | State::HeartBeat(fut) => match fut.poll_unpin(cx) {
228                    Poll::Pending => {
229                        return Poll::Pending;
230                    }
231                    Poll::Ready(Err(e)) => {
232                        warn!(error = ?e, "poll_ready: HeartBeat failed -> Dead");
233                        self.state = State::Dead;
234                        return Poll::Ready(Err(e));
235                    }
236                    Poll::Ready(Ok(())) => {
237                        debug!(
238                            ?heartbeat_interval,
239                            "poll_ready: HeartBeat complete, arming heartbeat timer"
240                        );
241                        worker.heartbeat(cx);
242                        self.heartbeat_timer = Some(Delay::new(heartbeat_interval));
243                        self.state = State::Ready;
244                    }
245                },
246
247                State::ProcessEvents(fut) => match fut.poll_unpin(cx) {
248                    Poll::Pending => {
249                        return Poll::Pending;
250                    }
251                    Poll::Ready(Err(e)) => {
252                        warn!(error = ?e, "poll_ready: ProcessEvents failed -> Dead");
253                        self.state = State::Dead;
254                        return Poll::Ready(Err(e));
255                    }
256                    Poll::Ready(Ok(())) => {
257                        trace!("poll_ready: ProcessEvents complete -> Ready");
258                        self.state = State::Ready;
259                    }
260                },
261
262                State::Ready => {
263                    let heartbeat_due = Pin::new(self.heartbeat_timer.as_mut().unwrap())
264                        .poll(cx)
265                        .is_ready();
266
267                    trace!(
268                        heartbeat_due,
269                        ?heartbeat_interval,
270                        "poll_ready: State::Ready evaluating heartbeat timer"
271                    );
272
273                    if heartbeat_due {
274                        debug!("poll_ready: heartbeat due -> dispatching HeartBeat future");
275                        let mut provider = self.inner.clone();
276                        let worker = worker.clone();
277                        let fut = async move {
278                            provider.heartbeat(&worker).await?;
279                            Ok(())
280                        };
281                        self.state = State::HeartBeat(fut.boxed().into());
282                        continue;
283                    }
284
285                    if let Some(events) = self.try_drain_events(cx) {
286                        debug!(
287                            count = events.len(),
288                            "poll_ready: draining events -> dispatching ProcessEvents future"
289                        );
290                        let mut provider = self.inner.clone();
291                        let worker = worker.clone();
292                        let fut = async move { provider.handle_events(events, &worker).await };
293                        self.state = State::ProcessEvents(fut.boxed().into());
294                        continue;
295                    }
296
297                    trace!("poll_ready: no heartbeat/events pending -> Ready(Ok)");
298                    return Poll::Ready(Ok(()));
299                }
300
301                _ => {
302                    trace!(state = ?self.state, "poll_ready: state handled elsewhere -> Ready(Ok)");
303                    return Poll::Ready(Ok(()));
304                }
305            }
306        }
307    }
308
309    /// Pulls the next task from the backend, fetching a fresh batch when the
310    /// internal buffer is exhausted.
311    ///
312    /// Assumes [`poll_ready`](Self::poll_ready) has already driven the state
313    /// machine into `State::Ready`; other states fall through to `Pending`
314    /// since they're the responsibility of `poll_ready`/`poll_close`.
315    #[allow(clippy::type_complexity)]
316    pub fn poll_next(
317        &mut self,
318        cx: &mut Context<'_>,
319        worker: &WorkerContext,
320    ) -> Poll<Option<Result<Task<P::Compact>, P::Error>>> {
321        trace!("poll_next: poll: {state:?}", state = self.state);
322        loop {
323            match &mut self.state {
324                State::Ready => {
325                    trace!("poll_next: State::Ready -> dispatching Fetch future");
326                    let mut provider = self.inner.clone();
327                    let worker = worker.clone();
328                    let fut = async move { provider.fetch_next(&worker).await };
329                    self.state = State::Fetch(fut.boxed().into());
330                }
331
332                State::Fetch(fut) => match fut.poll_unpin(cx) {
333                    Poll::Pending => {
334                        return Poll::Pending;
335                    }
336                    Poll::Ready(Ok(tasks)) if tasks.is_empty() => {
337                        trace!(
338                            "poll_next: fetch_next returned empty batch -> Ready, parking waker"
339                        );
340                        self.state = State::Ready;
341                        worker.register_waker(cx);
342                        return Poll::Pending;
343                    }
344                    Poll::Ready(Ok(tasks)) => {
345                        debug!(count = tasks.len(), "poll_next: fetch_next returned batch");
346                        self.state = State::Buffered(VecDeque::from(tasks));
347                    }
348                    Poll::Ready(Err(e)) => {
349                        warn!(error = ?e, "poll_next: fetch_next failed -> Dead");
350                        self.state = State::Dead;
351                        return Poll::Ready(Some(Err(e)));
352                    }
353                },
354
355                State::Buffered(buffer) => {
356                    if let Some(task) = buffer.pop_front() {
357                        if buffer.is_empty() {
358                            trace!("poll_next: buffer exhausted after pop -> Ready");
359                            self.state = State::Ready;
360                        }
361                        trace!(task_id = ?task.task_id(), "poll_next: yielding task from buffer");
362                        return Poll::Ready(Some(Ok(task)));
363                    }
364                    trace!("poll_next: buffer already empty -> Ready");
365                    self.state = State::Ready;
366                }
367
368                State::Dead => {
369                    debug!("poll_next: State::Dead -> Ready(None)");
370                    return Poll::Ready(None);
371                }
372
373                // RegisterWorker/HeartBeat/ProcessEvents/NotBootstrapped are
374                // poll_ready's responsibility — if we land here, the caller
375                // skipped poll_ready or called poll_next while it was still Pending.
376                _ => {
377                    error!(state = ?self.state, "poll_next: state not owned by poll_next -> Panic");
378                    unreachable!("poll_ready should have been called first");
379                }
380            }
381        }
382    }
383
384    /// Gracefully winds down the provider: finishes in-flight bootstrap/heartbeat
385    /// event futures, re-enqueues any abandoned buffered tasks, then flushes
386    /// remaining events before signalling completion.
387    pub fn poll_close(
388        &mut self,
389        cx: &mut Context<'_>,
390        worker: &WorkerContext,
391    ) -> Poll<Result<(), P::Error>> {
392        trace!("poll_close: Start: {:?}", self.state);
393        loop {
394            match &mut self.state {
395                // Let any in-flight future finish before we start forcing drains
396                State::RegisterWorker(fut) | State::HeartBeat(fut) => {
397                    poll_fut_or_return!(fut, cx);
398                    debug!("poll_close: in-flight RegisterWorker/HeartBeat completed");
399                    worker.heartbeat(cx);
400                    self.state = State::Ready;
401                }
402
403                State::ProcessEvents(fut) => {
404                    poll_fut_or_return!(fut, cx);
405                    trace!("poll_close: ProcessEvents complete -> Ready");
406                    self.state = State::Ready;
407                }
408
409                State::ReenqueueOrphaned(fut) => {
410                    poll_fut_or_return!(fut, cx);
411                    debug!("poll_close: ReenqueueOrphaned complete -> Ready");
412                    self.state = State::Ready;
413                }
414
415                State::Buffered(remaining) => {
416                    let mut provider = self.inner.clone();
417                    let worker = worker.clone();
418
419                    if !remaining.is_empty() {
420                        debug!(
421                            count = remaining.len(),
422                            "poll_close: re-enqueuing abandoned buffered tasks"
423                        );
424                        let tasks = Vec::from(std::mem::take(remaining));
425                        let fut = async move { provider.reenqueue_abandoned(tasks, &worker).await };
426                        self.state = State::ReenqueueOrphaned(fut.boxed().into());
427                    } else {
428                        trace!("poll_close: buffer already empty -> Ready");
429                        self.state = State::Ready;
430                    }
431                }
432
433                State::Ready => {
434                    let mut provider = self.inner.clone();
435                    let worker = worker.clone();
436
437                    if let Some(events) = self.try_drain_events(cx) {
438                        debug!(
439                            count = events.len(),
440                            "poll_close: flushing remaining events before shutdown"
441                        );
442                        let fut = async move { provider.handle_events(events, &worker).await };
443                        self.state = State::ProcessEvents(fut.boxed().into());
444                        continue;
445                    }
446                    debug!("poll_close: no remaining work -> Ready(Ok)");
447                    return Poll::Ready(Ok(()));
448                }
449
450                _ => {
451                    trace!("poll_close: unexpected state -> forcing Ready");
452                    self.state = State::Ready;
453                }
454            }
455        }
456    }
457}