apalis_core/worker/
lifecycle.rs1use std::{
11 pin::Pin,
12 sync::atomic::Ordering,
13 task::{Context, Poll},
14};
15
16use futures_util::FutureExt;
17use tower_layer::Layer;
18use tower_service::Service;
19
20use crate::{
21 error::{AbortError, BoxDynError, DeferredError, RetryAfterError},
22 task::{
23 Task,
24 attempt::Attempt,
25 status::{AtomicStatus, Status},
26 },
27 worker::context::WorkerContext,
28};
29
30#[cfg(feature = "task-context")]
31use crate::task::context::{TaskContext, TaskStateError};
32
33#[derive(Debug, Clone)]
39pub struct LifecycleLayer {
40 worker: WorkerContext,
41}
42
43impl LifecycleLayer {
44 pub(super) fn new(worker: WorkerContext) -> Self {
45 Self { worker }
46 }
47}
48
49impl<S> Layer<S> for LifecycleLayer {
50 type Service = LifecycleService<S>;
51
52 fn layer(&self, service: S) -> Self::Service {
53 LifecycleService {
54 worker: self.worker.clone(),
55 service,
56 }
57 }
58}
59
60#[derive(Debug, Clone)]
66pub struct LifecycleService<S> {
67 worker: WorkerContext,
68 service: S,
69}
70
71impl<S, Args> Service<Task<Args>> for LifecycleService<S>
72where
73 S: Service<Task<Args>>,
74 S::Error: Into<BoxDynError>,
75{
76 type Response = S::Response;
77 type Error = BoxDynError;
78 type Future = LifecycleFuture<S::Future>;
79
80 fn poll_ready(&mut self, cx: &mut std::task::Context<'_>) -> Poll<Result<(), Self::Error>> {
81 let result = self.service.poll_ready(cx).map_err(Into::into);
82 if self.worker.is_shutting_down() || self.worker.is_paused() || result.is_pending() {
83 self.worker.is_ready.store(false, Ordering::SeqCst);
84 return Poll::Pending;
85 }
86
87 match &result {
88 Poll::Ready(Ok(_)) => self.worker.is_ready.store(true, Ordering::SeqCst),
89 Poll::Pending | Poll::Ready(Err(_)) => {
90 self.worker.is_ready.store(false, Ordering::SeqCst)
91 }
92 }
93
94 result
95 }
96
97 fn call(&mut self, task: Task<Args>) -> Self::Future {
98 let attempt = task.raw_attempt().clone();
99 let status = task.raw_status().clone();
100 #[allow(unused, clippy::let_unit_value)]
101 let task_ctx = self
102 .worker
103 .register_task(task.ctx())
104 .expect("Could not register task");
105
106 LifecycleFuture {
107 fut: self.service.call(task),
108 worker: self.worker.clone(),
109 #[cfg(feature = "task-context")]
110 task_ctx,
111 #[cfg(not(feature = "task-context"))]
112 task_ctx: (),
113 record_attempt: Some(attempt),
114 task_status: status,
115 }
116 }
117}
118
119#[pin_project::pin_project(PinnedDrop)]
124#[derive(Debug)]
125pub struct LifecycleFuture<Fut> {
126 #[pin]
127 fut: Fut,
128 #[cfg(feature = "task-context")]
129 task_ctx: TaskContext,
130 #[cfg(not(feature = "task-context"))]
131 task_ctx: (),
132 worker: WorkerContext,
133 record_attempt: Option<Attempt>,
134 task_status: AtomicStatus,
135}
136
137#[derive(Debug, thiserror::Error)]
142#[non_exhaustive]
143pub enum TaskLifecycleError {
144 #[error("task doesn't have an id")]
149 MissingTaskId,
150
151 #[error("task already has a token")]
156 Duplicate,
157
158 #[cfg(feature = "task-context")]
159 #[error("task exited after manually being canceled")]
164 Exit(TaskContext),
165
166 #[cfg(feature = "task-context")]
167 #[error("task state is invalid: {0}")]
172 StateError(#[from] TaskStateError),
173}
174
175impl<Fut, T, E> Future for LifecycleFuture<Fut>
176where
177 Fut: Future<Output = Result<T, E>>,
178 E: Into<BoxDynError>,
179{
180 type Output = Result<T, BoxDynError>;
181
182 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
183 let mut this = self.project();
184 if let Some(attempt) = this.record_attempt.take() {
185 let current = attempt.increment();
186 this.task_status.store(Status::Running);
187 trace!("task started with prev attempts: {}", current);
188 }
189
190 #[cfg(feature = "task-context")]
191 if this.task_ctx.is_cancelled() {
192 trace!("task.cancelled");
193 this.task_status.store(Status::Killed);
194 return Poll::Ready(Err(Box::new(AbortError::new(TaskLifecycleError::Exit(
195 this.task_ctx.clone(),
196 )))));
197 }
198
199 match this.fut.poll_unpin(cx).map(|res| res.map_err(Into::into)) {
200 Poll::Pending => Poll::Pending,
201 Poll::Ready(res) => {
202 let new_status = match res {
203 Ok(_) => Status::Done,
204 Err(ref e) => match e {
205 e if e.downcast_ref::<AbortError>().is_some() => Status::Killed,
206 e if (e.downcast_ref::<RetryAfterError>().is_some()
207 || e.downcast_ref::<DeferredError>().is_some()) =>
208 {
209 Status::Pending
210 }
211 _ => Status::Failed,
212 },
213 };
214
215 #[cfg(feature = "task-context")]
216 if let Err(e) = this.task_ctx.complete() {
217 this.task_status.store(Status::Killed);
218 return Poll::Ready(Err(e.into()));
219 }
220 this.task_status.store(new_status);
221 Poll::Ready(res)
222 }
223 }
224 }
225}
226
227#[pin_project::pinned_drop]
228impl<Fut> PinnedDrop for LifecycleFuture<Fut> {
229 fn drop(self: Pin<&mut Self>) {
230 let this = self.project();
231 if this.worker.remove_task(this.task_ctx).is_err() {
232 warn!("Dropped a task without removing it from the worker's context");
233 }
234 if this.worker.task_count() == 0 {
235 this.worker.wake();
236 }
237 }
238}