@@ -30,7 +30,7 @@ use std::future::Future;
3030use std:: marker:: PhantomData ;
3131use std:: sync:: Arc ;
3232use std:: time:: Duration ;
33- use tracing:: { debug, error} ;
33+ use tracing:: { debug, error, warn } ;
3434
3535use crate :: dependency:: { Cons , Nil } ;
3636
@@ -82,7 +82,10 @@ pub trait Job: Serialize + DeserializeOwned + Send + 'static {
8282/// Built from [`Cons`](crate::Cons) / [`Nil`](crate::Nil) -- the same
8383/// cells [`deps!`](crate::deps) uses for reactor dependencies -- via
8484/// the [`jobs!`](crate::jobs) macro: `jobs![SendEmail, ChargeCard]`.
85- pub trait JobList { }
85+ ///
86+ /// `Send` so the [`JobQueue`] threads through the framework's `Send`
87+ /// command future.
88+ pub trait JobList : Send { }
8689
8790impl JobList for Nil { }
8891
@@ -455,22 +458,19 @@ macro_rules! build_supervised_worker {
455458 } } ;
456459}
457460
458- /// Drains a [`JobQueue`] into apalis's `Jobs` table through `connection`.
461+ /// Writes buffered pushes into apalis's `Jobs` table through `connection`.
459462///
460- /// Called by the framework inside the event-commit transaction so a
461- /// job becomes visible to the worker iff its triggering events commit.
462463/// Rows are written in apalis's storage format -- JSON-encoded payload,
463464/// `Pending` status, the job type as the queue name -- so the worker's
464465/// fetcher picks them up.
465- #[ doc( hidden) ]
466- pub async fn flush_jobs < Jobs : JobList > (
466+ async fn insert_pending (
467467 connection : & mut sqlx:: SqliteConnection ,
468- queue : JobQueue < Jobs > ,
468+ pending : Vec < PendingPush > ,
469469) -> Result < ( ) , QueuePushError > {
470- for pending in queue . into_pending ( ) {
471- let job_type = pending . job . job_type ( ) ;
472- let payload = pending . job . encode ( ) ?;
473- let delay_secs = match pending . delay {
470+ for push in pending {
471+ let job_type = push . job . job_type ( ) ;
472+ let payload = push . job . encode ( ) ?;
473+ let delay_secs = match push . delay {
474474 None => 0_i64 ,
475475 Some ( delay) => i64:: try_from ( delay. as_secs ( ) ) ?,
476476 } ;
@@ -491,6 +491,72 @@ pub async fn flush_jobs<Jobs: JobList>(
491491 Ok ( ( ) )
492492}
493493
494+ /// Drains a [`JobQueue`] into apalis's `Jobs` table through `connection`.
495+ ///
496+ /// Called by the framework inside the event-commit transaction so a job
497+ /// becomes visible to the worker iff its triggering events commit.
498+ #[ doc( hidden) ]
499+ pub async fn flush_jobs < Jobs : JobList > (
500+ connection : & mut sqlx:: SqliteConnection ,
501+ queue : JobQueue < Jobs > ,
502+ ) -> Result < ( ) , QueuePushError > {
503+ insert_pending ( connection, queue. into_pending ( ) ) . await
504+ }
505+
506+ tokio:: task_local! {
507+ /// Per-command buffer of jobs awaiting flush. Scoped by
508+ /// [`with_pending_jobs`] around command execution, populated by the
509+ /// [`Lifecycle`](crate::Lifecycle) handler, and drained by the event
510+ /// repository inside its commit transaction.
511+ static PENDING_JOBS : std:: cell:: RefCell <Vec <PendingPush >>;
512+ }
513+
514+ /// Runs `future` with a fresh per-command pending-jobs buffer in scope, so
515+ /// the jobs a handler enqueues are flushed in the same transaction that
516+ /// commits its events.
517+ pub ( crate ) async fn with_pending_jobs < F > ( future : F ) -> F :: Output
518+ where
519+ F : Future ,
520+ {
521+ PENDING_JOBS
522+ . scope ( std:: cell:: RefCell :: new ( Vec :: new ( ) ) , future)
523+ . await
524+ }
525+
526+ /// Buffers a handler's [`JobQueue`] for the framework to flush at commit.
527+ ///
528+ /// A no-op when the queue is empty. If jobs are present but no
529+ /// [`with_pending_jobs`] scope is active (a programming error -- handlers
530+ /// run inside [`Store::send`](crate::Store::send)), they are dropped with a
531+ /// warning rather than silently lost.
532+ pub ( crate ) fn buffer_pending < Jobs : JobList > ( queue : JobQueue < Jobs > ) {
533+ let pending = queue. into_pending ( ) ;
534+ if pending. is_empty ( ) {
535+ return ;
536+ }
537+
538+ let count = pending. len ( ) ;
539+ if PENDING_JOBS
540+ . try_with ( move |cell| cell. borrow_mut ( ) . extend ( pending) )
541+ . is_err ( )
542+ {
543+ warn ! ( target: "job" , count, "jobs enqueued outside a command scope were dropped" ) ;
544+ }
545+ }
546+
547+ /// Flushes the current command's buffered jobs through `connection`.
548+ ///
549+ /// Called by the event repository inside its commit transaction; a no-op
550+ /// when no scope is active or nothing was buffered.
551+ pub ( crate ) async fn flush_pending_jobs (
552+ connection : & mut sqlx:: SqliteConnection ,
553+ ) -> Result < ( ) , QueuePushError > {
554+ let pending = PENDING_JOBS
555+ . try_with ( |cell| std:: mem:: take ( & mut * cell. borrow_mut ( ) ) )
556+ . unwrap_or_default ( ) ;
557+ insert_pending ( connection, pending) . await
558+ }
559+
494560fn build_poll_config < J : ' static > ( ) -> Config {
495561 let strategy = StrategyBuilder :: new ( )
496562 . apply (
0 commit comments