@@ -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
@@ -88,7 +88,10 @@ pub trait Job: Serialize + DeserializeOwned + Send + 'static {
8888/// Built from [`Cons`](crate::Cons) / [`Nil`](crate::Nil) -- the same
8989/// cells [`deps!`](crate::deps) uses for reactor dependencies -- via
9090/// the [`jobs!`](crate::jobs) macro: `jobs![SendEmail, ChargeCard]`.
91- pub trait JobList { }
91+ ///
92+ /// `Send` so the [`JobQueue`] threads through the framework's `Send`
93+ /// command future.
94+ pub trait JobList : Send { }
9295
9396impl JobList for Nil { }
9497
@@ -459,22 +462,19 @@ macro_rules! build_supervised_worker {
459462 } } ;
460463}
461464
462- /// Drains a [`JobQueue`] into apalis's `Jobs` table through `connection`.
465+ /// Writes buffered pushes into apalis's `Jobs` table through `connection`.
463466///
464- /// Called by the framework inside the event-commit transaction so a
465- /// job becomes visible to the worker iff its triggering events commit.
466467/// Rows are written in apalis's storage format -- JSON-encoded payload,
467468/// `Pending` status, the job type as the queue name -- so the worker's
468469/// fetcher picks them up.
469- #[ doc( hidden) ]
470- pub async fn flush_jobs < Jobs : JobList > (
470+ async fn insert_pending (
471471 connection : & mut sqlx:: SqliteConnection ,
472- queue : JobQueue < Jobs > ,
472+ pending : Vec < PendingPush > ,
473473) -> Result < ( ) , QueuePushError > {
474- for pending in queue . into_pending ( ) {
475- let job_type = pending . job . job_type ( ) ;
476- let payload = pending . job . encode ( ) ?;
477- let delay_secs = match pending . delay {
474+ for push in pending {
475+ let job_type = push . job . job_type ( ) ;
476+ let payload = push . job . encode ( ) ?;
477+ let delay_secs = match push . delay {
478478 None => 0_i64 ,
479479 Some ( delay) => i64:: try_from ( delay. as_secs ( ) ) ?,
480480 } ;
@@ -495,6 +495,72 @@ pub async fn flush_jobs<Jobs: JobList>(
495495 Ok ( ( ) )
496496}
497497
498+ /// Drains a [`JobQueue`] into apalis's `Jobs` table through `connection`.
499+ ///
500+ /// Called by the framework inside the event-commit transaction so a job
501+ /// becomes visible to the worker iff its triggering events commit.
502+ #[ doc( hidden) ]
503+ pub async fn flush_jobs < Jobs : JobList > (
504+ connection : & mut sqlx:: SqliteConnection ,
505+ queue : JobQueue < Jobs > ,
506+ ) -> Result < ( ) , QueuePushError > {
507+ insert_pending ( connection, queue. into_pending ( ) ) . await
508+ }
509+
510+ tokio:: task_local! {
511+ /// Per-command buffer of jobs awaiting flush. Scoped by
512+ /// [`with_pending_jobs`] around command execution, populated by the
513+ /// [`Lifecycle`](crate::Lifecycle) handler, and drained by the event
514+ /// repository inside its commit transaction.
515+ static PENDING_JOBS : std:: cell:: RefCell <Vec <PendingPush >>;
516+ }
517+
518+ /// Runs `future` with a fresh per-command pending-jobs buffer in scope, so
519+ /// the jobs a handler enqueues are flushed in the same transaction that
520+ /// commits its events.
521+ pub ( crate ) async fn with_pending_jobs < F > ( future : F ) -> F :: Output
522+ where
523+ F : Future ,
524+ {
525+ PENDING_JOBS
526+ . scope ( std:: cell:: RefCell :: new ( Vec :: new ( ) ) , future)
527+ . await
528+ }
529+
530+ /// Buffers a handler's [`JobQueue`] for the framework to flush at commit.
531+ ///
532+ /// A no-op when the queue is empty. If jobs are present but no
533+ /// [`with_pending_jobs`] scope is active (a programming error -- handlers
534+ /// run inside [`Store::send`](crate::Store::send)), they are dropped with a
535+ /// warning rather than silently lost.
536+ pub ( crate ) fn buffer_pending < Jobs : JobList > ( queue : JobQueue < Jobs > ) {
537+ let pending = queue. into_pending ( ) ;
538+ if pending. is_empty ( ) {
539+ return ;
540+ }
541+
542+ let count = pending. len ( ) ;
543+ if PENDING_JOBS
544+ . try_with ( move |cell| cell. borrow_mut ( ) . extend ( pending) )
545+ . is_err ( )
546+ {
547+ warn ! ( target: "job" , count, "jobs enqueued outside a command scope were dropped" ) ;
548+ }
549+ }
550+
551+ /// Flushes the current command's buffered jobs through `connection`.
552+ ///
553+ /// Called by the event repository inside its commit transaction; a no-op
554+ /// when no scope is active or nothing was buffered.
555+ pub ( crate ) async fn flush_pending_jobs (
556+ connection : & mut sqlx:: SqliteConnection ,
557+ ) -> Result < ( ) , QueuePushError > {
558+ let pending = PENDING_JOBS
559+ . try_with ( |cell| std:: mem:: take ( & mut * cell. borrow_mut ( ) ) )
560+ . unwrap_or_default ( ) ;
561+ insert_pending ( connection, pending) . await
562+ }
563+
498564fn build_poll_config < J : Job > ( ) -> Config {
499565 let strategy = StrategyBuilder :: new ( )
500566 . apply (
0 commit comments