1use std::{
2 collections::VecDeque,
3 marker::PhantomData,
4 pin::Pin,
5 sync::{
6 Arc,
7 atomic::{AtomicUsize, Ordering},
8 },
9 task::{Context, Poll},
10 time::Duration,
11};
12
13use apalis_core::{
14 backend::{
15 TaskStream,
16 codec::Codec,
17 poll_strategy::{PollContext, PollStrategyExt},
18 },
19 task::Task,
20 timer::Delay,
21 worker::context::WorkerContext,
22};
23use futures::{
24 FutureExt, Stream, StreamExt, TryFutureExt,
25 future::{BoxFuture, ready},
26 stream,
27};
28
29use crate::{CompactType, Config, Error, PgContext, PgPool, PgTask, PgTaskId, queries};
30
31#[derive(Debug, Clone, Default)]
33pub struct PgNotify;
34
35pub(crate) fn register_then_stream<S>(
46 register: impl Future<Output = Result<Option<PgTask<CompactType>>, Error>> + Send + 'static,
47 body: S,
48) -> TaskStream<PgTask<CompactType>, Error>
49where
50 S: Stream<Item = Result<Option<PgTask<CompactType>>, Error>> + Send + 'static,
51{
52 let mut body_slot = Some(body);
53 stream::once(register)
54 .flat_map(move |res| match res {
55 Ok(none) => {
56 let b = body_slot
57 .take()
58 .expect("registration flat_map invoked twice");
59 stream::once(ready(Ok(none))).chain(b).left_stream()
60 }
61 Err(e) => stream::once(ready(Err(e))).right_stream(),
62 })
63 .boxed()
64}
65
66pub(crate) fn decode_task_stream<Args, Decode>(
77 compact: TaskStream<PgTask<CompactType>, Error>,
78 pool: PgPool,
79 worker_id: String,
80) -> TaskStream<PgTask<Args>, Error>
81where
82 Args: Send + 'static,
83 Decode: Codec<Args, Compact = CompactType> + 'static,
84 Decode::Error: std::error::Error + Send + Sync + 'static,
85{
86 compact
87 .then(move |row| {
88 let pool = pool.clone();
89 let worker_id = worker_id.clone();
90 async move {
91 match row {
92 Ok(Some(task)) => {
93 let task_id = task.parts.task_id;
98 let lock_at = *task.parts.ctx.lock_at();
99 let attempts = i32::try_from(task.parts.attempt.current());
100 match task
101 .try_map(|t| Decode::decode(&t).map_err(|e| Error::Decode(e.into())))
102 {
103 Ok(decoded) => Ok(Some(decoded)),
104 Err(error) => {
105 if let (Some(task_id), Some(lock_at), Ok(attempts)) =
111 (task_id, lock_at, attempts)
112 {
113 let _ = queries::fail_undecodable_task(
114 pool,
115 task_id,
116 worker_id,
117 lock_at,
118 attempts,
119 error.to_string(),
120 )
121 .await;
122 }
123 Err(error)
124 }
125 }
126 }
127 Ok(None) => Ok(None),
128 Err(error) => Err(error),
129 }
130 }
131 })
132 .boxed()
133}
134
135impl PgFetcherSource for PgNotify {
136 const STORAGE_NAME: &'static str = "PostgresStorageWithNotify";
137
138 fn into_compact_stream(
139 self,
140 pool: PgPool,
141 config: Config,
142 worker: WorkerContext,
143 lease_token: Arc<str>,
144 ) -> TaskStream<PgTask<CompactType>, Error> {
145 let ids = queries::notify_task_ids(
146 pool.clone(),
147 config.queue().to_string(),
148 config.buffer_size().max(1),
149 );
150 notify_backed_compact_stream(Self::STORAGE_NAME, ids, pool, config, worker, lease_token)
151 }
152}
153
154pub(crate) fn notify_backed_compact_stream<Ids>(
167 storage_name: &'static str,
168 ids: Ids,
169 pool: PgPool,
170 config: Config,
171 worker: WorkerContext,
172 lease_token: Arc<str>,
173) -> TaskStream<PgTask<CompactType>, Error>
174where
175 Ids: Stream<Item = Result<PgTaskId, Error>> + Send + 'static,
176{
177 let register_worker = queries::initial_heartbeat(
178 pool.clone(),
179 config.clone(),
180 worker.clone(),
181 storage_name,
182 lease_token,
183 )
184 .map_ok(|_| None);
185
186 let lazy_fetcher = queries::batch_ids_into_tasks(
187 pool.clone(),
188 config.queue().to_string(),
189 worker.name().to_owned(),
190 config.buffer_size().max(1),
191 ids,
192 )
193 .boxed();
194
195 let eager_fetcher = PgPollFetcher::<CompactType>::new(&pool, &config, &worker);
196 let combined = futures::stream::select(lazy_fetcher, eager_fetcher);
197 register_then_stream(register_worker, combined)
198}
199
200pub(crate) trait PgFetcherSource: Sized + Send + 'static {
207 const STORAGE_NAME: &'static str;
208
209 fn into_compact_stream(
210 self,
211 pool: PgPool,
212 config: Config,
213 worker: apalis_core::worker::context::WorkerContext,
214 lease_token: Arc<str>,
215 ) -> TaskStream<PgTask<CompactType>, Error>;
216}
217
218impl<Decode> PgFetcherSource for PgFetcher<CompactType, Decode>
219where
220 Decode: Send + 'static,
221{
222 const STORAGE_NAME: &'static str = crate::STORAGE_NAME;
223
224 fn into_compact_stream(
225 self,
226 pool: PgPool,
227 config: Config,
228 worker: apalis_core::worker::context::WorkerContext,
229 lease_token: Arc<str>,
230 ) -> TaskStream<PgTask<CompactType>, Error> {
231 let register_worker = queries::initial_heartbeat(
232 pool.clone(),
233 config.clone(),
234 worker.clone(),
235 Self::STORAGE_NAME,
236 lease_token,
237 )
238 .map_ok(|_| None);
239 let fetcher = PgPollFetcher::<CompactType>::new(&pool, &config, &worker);
240 register_then_stream(register_worker, fetcher)
241 }
242}
243
244type Poller = Pin<Box<dyn Stream<Item = ()> + Send>>;
245
246enum StreamState<Args> {
247 WaitForPoll(Poller),
248 StrategyEnded(Delay),
249 Fetch(BoxFuture<'static, Result<Vec<PgTask<Args>>, Error>>),
250 Buffered(VecDeque<PgTask<Args>>),
251}
252
253#[derive(Clone, Debug, Default)]
255pub struct PgFetcher<Compact, Decode> {
256 _marker: PhantomData<(Compact, Decode)>,
257}
258
259pub(crate) struct PgPollFetcher<Compact> {
261 pool: PgPool,
262 config: Config,
263 worker: WorkerContext,
264 state: StreamState<Compact>,
265 previous_task_count: Arc<AtomicUsize>,
266}
267
268impl<Compact> Clone for PgPollFetcher<Compact> {
269 fn clone(&self) -> Self {
270 let previous_task_count = Arc::new(AtomicUsize::new(0));
271 Self {
272 pool: self.pool.clone(),
273 config: self.config.clone(),
274 worker: self.worker.clone(),
275 state: poll_state(&self.config, &self.worker, previous_task_count.clone()),
276 previous_task_count,
277 }
278 }
279}
280
281impl PgPollFetcher<CompactType> {
282 #[must_use]
284 pub fn new(pool: &PgPool, config: &Config, worker: &WorkerContext) -> Self {
285 let previous_task_count = Arc::new(AtomicUsize::new(0));
286 Self {
287 pool: pool.clone(),
288 config: config.clone(),
289 worker: worker.clone(),
290 state: poll_state(config, worker, previous_task_count.clone()),
291 previous_task_count,
292 }
293 }
294}
295
296const STRATEGY_EXHAUSTED_BACKOFF: Duration = Duration::from_millis(100);
301
302impl PgPollFetcher<CompactType> {
303 fn start_fetch(&self) -> StreamState<CompactType> {
304 StreamState::Fetch(
305 queries::fetch_next(self.pool.clone(), self.config.clone(), self.worker.clone())
306 .boxed(),
307 )
308 }
309}
310
311impl<Compact> PgPollFetcher<Compact> {
312 #[cfg(test)]
315 #[must_use]
316 pub(crate) fn take_pending(&mut self) -> VecDeque<PgTask<Compact>> {
317 match &mut self.state {
318 StreamState::Buffered(tasks) => std::mem::take(tasks),
319 _ => VecDeque::new(),
320 }
321 }
322}
323
324impl Stream for PgPollFetcher<CompactType> {
325 type Item = Result<Option<Task<CompactType, PgContext, ulid::Ulid>>, Error>;
326
327 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
328 let this = self.get_mut();
329
330 loop {
331 match &mut this.state {
332 StreamState::WaitForPoll(poller) => match poller.poll_next_unpin(cx) {
333 Poll::Pending => return Poll::Pending,
334 Poll::Ready(Some(())) => {
335 this.state = this.start_fetch();
336 }
337 Poll::Ready(None) => {
338 this.state =
339 StreamState::StrategyEnded(Delay::new(STRATEGY_EXHAUSTED_BACKOFF));
340 }
341 },
342 StreamState::StrategyEnded(delay) => match Pin::new(delay).poll(cx) {
343 Poll::Pending => return Poll::Pending,
344 Poll::Ready(()) => {
345 this.state = this.start_fetch();
346 }
347 },
348 StreamState::Fetch(fetch) => match fetch.poll_unpin(cx) {
349 Poll::Pending => return Poll::Pending,
350 Poll::Ready(Ok(tasks)) if tasks.is_empty() => {
351 this.previous_task_count.store(0, Ordering::Relaxed);
352 this.state = poll_state(
353 &this.config,
354 &this.worker,
355 this.previous_task_count.clone(),
356 );
357 }
358 Poll::Ready(Ok(tasks)) => {
359 this.previous_task_count
360 .store(tasks.len(), Ordering::Relaxed);
361 this.state = StreamState::Buffered(VecDeque::from(tasks));
362 }
363 Poll::Ready(Err(error)) => {
364 this.previous_task_count.store(0, Ordering::Relaxed);
365 this.state = poll_state(
366 &this.config,
367 &this.worker,
368 this.previous_task_count.clone(),
369 );
370 return Poll::Ready(Some(Err(error)));
371 }
372 },
373 StreamState::Buffered(buffer) => {
374 if let Some(task) = buffer.pop_front() {
375 if buffer.is_empty() {
376 this.state = poll_state(
377 &this.config,
378 &this.worker,
379 this.previous_task_count.clone(),
380 );
381 }
382 return Poll::Ready(Some(Ok(Some(task))));
383 }
384 this.state =
385 poll_state(&this.config, &this.worker, this.previous_task_count.clone());
386 }
387 }
388 }
389 }
390}
391
392fn poll_state<Compact>(
393 config: &Config,
394 worker: &WorkerContext,
395 previous_task_count: Arc<AtomicUsize>,
396) -> StreamState<Compact> {
397 let context = PollContext::new(worker.clone(), previous_task_count);
398 StreamState::WaitForPoll(config.poll_strategy().clone().build_stream(&context))
399}
400
401#[cfg(test)]
402mod tests {
403 use std::{
404 collections::VecDeque,
405 pin::Pin,
406 sync::{
407 Arc,
408 atomic::{AtomicUsize, Ordering},
409 },
410 task::{Context, Poll},
411 time::Duration,
412 };
413
414 use apalis_core::{task::builder::TaskBuilder, worker::context::WorkerContext};
415 use diesel::{
416 PgConnection,
417 r2d2::{ConnectionManager, Pool},
418 };
419 use futures::{FutureExt, future, stream, task::noop_waker_ref};
420 use lets_expect::{AssertionError, AssertionResult, *};
421
422 use super::*;
423
424 struct PollObservation {
425 poll: &'static str,
426 state: &'static str,
427 previous_task_count: usize,
428 }
429
430 fn unchecked_pool() -> PgPool {
431 let manager = ConnectionManager::<PgConnection>::new("postgres://127.0.0.1:1/not-used");
432 Pool::builder()
433 .max_size(1)
434 .connection_timeout(Duration::from_millis(10))
435 .build_unchecked(manager)
436 }
437
438 fn buffered_fetcher() -> PgPollFetcher<CompactType> {
439 PgPollFetcher {
440 pool: unchecked_pool(),
441 config: Config::new("fetcher-test"),
442 worker: WorkerContext::new::<()>("fetcher-worker"),
443 state: StreamState::Buffered(VecDeque::new()),
444 previous_task_count: Arc::new(AtomicUsize::new(12)),
445 }
446 }
447
448 fn state_name(fetcher: &PgPollFetcher<CompactType>) -> &'static str {
449 match &fetcher.state {
450 StreamState::WaitForPoll(_) => "wait_for_poll",
451 StreamState::StrategyEnded(_) => "strategy_ended",
452 StreamState::Fetch(_) => "fetch",
453 StreamState::Buffered(_) => "buffered",
454 }
455 }
456
457 fn poll_observation(fetcher: &mut PgPollFetcher<CompactType>) -> PollObservation {
458 let mut cx = Context::from_waker(noop_waker_ref());
459 let poll = match Pin::new(&mut *fetcher).poll_next(&mut cx) {
460 Poll::Ready(Some(Ok(Some(_)))) => "task",
461 Poll::Ready(Some(Ok(None))) => "empty",
462 Poll::Ready(Some(Err(_))) => "error",
463 Poll::Ready(None) => "closed",
464 Poll::Pending => "pending",
465 };
466 PollObservation {
467 poll,
468 state: state_name(fetcher),
469 previous_task_count: fetcher.previous_task_count.load(Ordering::Relaxed),
470 }
471 }
472
473 fn pending_poll_strategy_observation() -> PollObservation {
474 let mut fetcher = buffered_fetcher();
475 fetcher.state = StreamState::WaitForPoll(Box::pin(stream::pending()));
476 poll_observation(&mut fetcher)
477 }
478
479 fn exhausted_poll_strategy_observation() -> PollObservation {
480 let mut fetcher = buffered_fetcher();
484 fetcher.state = StreamState::WaitForPoll(Box::pin(stream::empty::<()>()));
485 poll_observation(&mut fetcher)
486 }
487
488 fn observed_strategy_exhaustion(result: &PollObservation) -> AssertionResult {
489 match (result.poll, result.state) {
490 ("pending", "strategy_ended") => Ok(()),
494 other => Err(AssertionError::new(vec![format!(
495 "expected exhausted strategy to transition into strategy_ended/pending, got {other:?}"
496 )])),
497 }
498 }
499
500 fn fetch_error_observation() -> PollObservation {
501 let mut fetcher = buffered_fetcher();
502 fetcher.state = StreamState::Fetch(future::ready(Err(Error::SinkBufferFull(1))).boxed());
503 poll_observation(&mut fetcher)
504 }
505
506 fn empty_fetch_observation() -> PollObservation {
507 let mut fetcher = buffered_fetcher();
508 fetcher.state = StreamState::Fetch(future::ready(Ok(Vec::new())).boxed());
509 poll_observation(&mut fetcher)
510 }
511
512 fn successful_fetch_observation() -> PollObservation {
513 let mut fetcher = buffered_fetcher();
514 let task = TaskBuilder::new(vec![1, 2, 3])
515 .with_ctx(PgContext::new())
516 .build();
517 fetcher.state = StreamState::Fetch(future::ready(Ok(vec![task])).boxed());
518 poll_observation(&mut fetcher)
519 }
520
521 fn fetch_pending_observation() -> PollObservation {
522 let mut fetcher = buffered_fetcher();
523 fetcher.state = StreamState::Fetch(future::pending().boxed());
524 poll_observation(&mut fetcher)
525 }
526
527 fn cloned_state(fetcher: &PgPollFetcher<CompactType>) -> &'static str {
528 match &fetcher.clone().state {
529 StreamState::WaitForPoll(_) => "wait_for_poll",
530 StreamState::StrategyEnded(_) => "strategy_ended",
531 StreamState::Fetch(_) => "fetch",
532 StreamState::Buffered(_) => "buffered",
533 }
534 }
535
536 fn cloned_previous_task_count(fetcher: &PgPollFetcher<CompactType>) -> usize {
537 fetcher.clone().previous_task_count.load(Ordering::Relaxed)
538 }
539
540 fn observed_fetch_error(result: &PollObservation) -> AssertionResult {
541 match (result.poll, result.state, result.previous_task_count) {
542 ("error", "wait_for_poll", 0) => Ok(()),
543 other => Err(AssertionError::new(vec![format!(
544 "expected fetch error to reset the poll strategy, got {other:?}"
545 )])),
546 }
547 }
548
549 fn observed_empty_fetch(result: &PollObservation) -> AssertionResult {
550 match (result.poll, result.state, result.previous_task_count) {
551 ("pending", "wait_for_poll", 0) => Ok(()),
552 other => Err(AssertionError::new(vec![format!(
553 "expected empty fetch to wait for configured polling, got {other:?}"
554 )])),
555 }
556 }
557
558 fn observed_successful_fetch(result: &PollObservation) -> AssertionResult {
559 match (result.poll, result.state, result.previous_task_count) {
560 ("task", "wait_for_poll", 1) => Ok(()),
561 other => Err(AssertionError::new(vec![format!(
562 "expected successful fetch to yield one task and remember the count, got {other:?}"
563 )])),
564 }
565 }
566
567 fn observed_pending_fetch(result: &PollObservation) -> AssertionResult {
568 match (result.poll, result.state, result.previous_task_count) {
572 ("pending", "fetch", 12) => Ok(()),
573 other => Err(AssertionError::new(vec![format!(
574 "expected an in-flight fetch to wait without touching the batch count, got {other:?}"
575 )])),
576 }
577 }
578
579 fn observed_pending_strategy(result: &PollObservation) -> AssertionResult {
580 match (result.poll, result.state, result.previous_task_count) {
581 ("pending", "wait_for_poll", 12) => Ok(()),
582 other => Err(AssertionError::new(vec![format!(
583 "expected pending strategy to prevent a database fetch, got {other:?}"
584 )])),
585 }
586 }
587
588 fn buffered_with(tasks: Vec<PgTask<CompactType>>) -> PgPollFetcher<CompactType> {
589 let mut fetcher = buffered_fetcher();
590 fetcher.state = StreamState::Buffered(VecDeque::from(tasks));
591 fetcher
592 }
593
594 fn synthetic_task(payload: &[u8]) -> PgTask<CompactType> {
595 TaskBuilder::new(payload.to_vec())
596 .with_ctx(PgContext::new())
597 .build()
598 }
599
600 fn take_pending_count(state_kind: &'static str) -> usize {
601 let mut fetcher = match state_kind {
602 "buffered_two" => buffered_with(vec![synthetic_task(b"one"), synthetic_task(b"two")]),
603 "buffered_empty" => buffered_with(Vec::new()),
604 "wait_for_poll" => {
605 let mut fetcher = buffered_fetcher();
606 fetcher.state = StreamState::WaitForPoll(Box::pin(stream::pending()));
607 fetcher
608 }
609 "fetch" => {
610 let mut fetcher = buffered_fetcher();
611 fetcher.state = StreamState::Fetch(future::ready(Ok(Vec::new())).boxed());
612 fetcher
613 }
614 "strategy_ended" => {
615 let mut fetcher = buffered_fetcher();
616 fetcher.state = StreamState::StrategyEnded(Delay::new(Duration::from_secs(60)));
617 fetcher
618 }
619 other => panic!("unknown state kind: {other}"),
620 };
621 fetcher.take_pending().len()
622 }
623
624 fn take_pending_drains_then_reports_empty() -> (usize, usize, &'static str) {
629 let mut fetcher = buffered_with(vec![synthetic_task(b"alpha"), synthetic_task(b"beta")]);
630 let drained = fetcher.take_pending().len();
631 let remaining = match &fetcher.state {
632 StreamState::Buffered(tasks) => tasks.len(),
633 _ => panic!("take_pending changed the state slot"),
634 };
635 (drained, remaining, state_name(&fetcher))
636 }
637
638 fn buffered_pop_front_observation() -> PollObservation {
639 let mut fetcher = buffered_with(vec![synthetic_task(b"first"), synthetic_task(b"second")]);
640 poll_observation(&mut fetcher)
641 }
642
643 fn observed_buffered_pop_front(result: &PollObservation) -> AssertionResult {
644 match (result.poll, result.state, result.previous_task_count) {
649 ("task", "buffered", 12) => Ok(()),
650 other => Err(AssertionError::new(vec![format!(
651 "expected pop_front to yield a task while remaining buffered, got {other:?}"
652 )])),
653 }
654 }
655
656 fn buffered_drain_observation() -> &'static str {
660 let mut fetcher = buffered_with(vec![synthetic_task(b"only")]);
661 let mut cx = Context::from_waker(noop_waker_ref());
662 let _ = Pin::new(&mut fetcher).poll_next(&mut cx);
663 state_name(&fetcher)
664 }
665
666 lets_expect! {
667 expect(cloned_state(&fetcher)) {
668 let fetcher = buffered_fetcher();
669
670 when original_stream_has_buffered_state {
671 to resets_the_clone_to_poll_strategy { equal("wait_for_poll") }
672 }
673 }
674
675 expect(cloned_previous_task_count(&fetcher)) {
676 let fetcher = buffered_fetcher();
677
678 when original_stream_remembers_a_previous_batch {
679 to starts_the_clone_with_no_previous_count { equal(0) }
680 }
681 }
682
683 expect(pending_poll_strategy_observation()) {
684 when the_configured_poll_strategy_is_not_ready {
685 to does_not_start_a_fetch { observed_pending_strategy }
686 }
687 }
688
689 expect(exhausted_poll_strategy_observation()) {
690 when the_configured_poll_strategy_returns_ready_none {
691 to transitions_into_strategy_ended_and_waits_for_the_delay {
692 observed_strategy_exhaustion
693 }
694 }
695 }
696
697 expect(fetch_error_observation()) {
698 when fetch_query_fails {
699 to yields_the_error_and_waits_for_the_next_poll_signal { observed_fetch_error }
700 }
701 }
702
703 expect(empty_fetch_observation()) {
704 when fetch_returns_no_tasks {
705 to waits_for_the_next_configured_poll_signal { observed_empty_fetch }
706 }
707 }
708
709 expect(successful_fetch_observation()) {
710 when fetch_returns_tasks {
711 to yields_a_task_and_records_the_batch_size { observed_successful_fetch }
712 }
713 }
714
715 expect(fetch_pending_observation()) {
716 when fetch_query_is_still_in_flight {
717 to waits_without_touching_the_batch_count { observed_pending_fetch }
718 }
719 }
720
721 expect(take_pending_count(state_kind)) {
722 let state_kind = "buffered_two";
723
724 when fetcher_is_in_buffered_state_with_two_tasks {
725 to drains_every_buffered_task { equal(2) }
726 }
727
728 when fetcher_is_in_buffered_state_with_no_tasks {
729 let state_kind = "buffered_empty";
730 to returns_an_empty_drained_queue { equal(0) }
731 }
732
733 when fetcher_is_in_wait_for_poll_state {
734 let state_kind = "wait_for_poll";
735 to ignores_states_other_than_buffered { equal(0) }
736 }
737
738 when fetcher_is_in_fetch_state {
739 let state_kind = "fetch";
740 to ignores_states_other_than_buffered { equal(0) }
741 }
742
743 when fetcher_is_in_strategy_ended_state {
744 let state_kind = "strategy_ended";
745 to ignores_states_other_than_buffered { equal(0) }
746 }
747 }
748
749 expect(take_pending_drains_then_reports_empty()) {
750 when buffered_state_is_drained_via_take_pending {
751 to leaves_the_fetcher_in_the_buffered_state_with_zero_tasks {
752 equal((2, 0, "buffered"))
753 }
754 }
755 }
756
757 expect(buffered_pop_front_observation()) {
758 when buffer_holds_multiple_tasks {
759 to pops_a_task_and_stays_in_buffered { observed_buffered_pop_front }
760 }
761 }
762
763 expect(buffered_drain_observation()) {
764 when buffer_holds_exactly_one_task {
765 to transitions_to_wait_for_poll_after_emitting_the_task {
766 equal("wait_for_poll")
767 }
768 }
769 }
770 }
771}