Skip to content

Commit 72cd628

Browse files
committed
feat: replace EventSourced::Services with Jobs and wire atomic enqueue
1 parent 577f213 commit 72cd628

17 files changed

Lines changed: 452 additions & 268 deletions

File tree

crates/event-sorcery/src/dependency.rs

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -382,11 +382,11 @@ macro_rules! register_entities {
382382

383383
#[cfg(test)]
384384
mod tests {
385-
use async_trait::async_trait;
386385
use cqrs_es::DomainEvent;
387386
use serde::{Deserialize, Serialize};
388387

389388
use super::*;
389+
use crate::JobQueue;
390390

391391
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
392392
struct Alpha {
@@ -411,13 +411,12 @@ mod tests {
411411
#[error("alpha error")]
412412
struct AlphaError;
413413

414-
#[async_trait]
415414
impl EventSourced for Alpha {
416415
type Id = String;
417416
type Event = AlphaEvent;
418417
type Command = ();
419418
type Error = AlphaError;
420-
type Services = ();
419+
type Jobs = Nil;
421420
type Materialized = Nil;
422421

423422
const AGGREGATE_TYPE: &'static str = "Alpha";
@@ -436,11 +435,18 @@ mod tests {
436435
Ok(None)
437436
}
438437

439-
async fn initialize((): (), (): &()) -> Result<Vec<AlphaEvent>, AlphaError> {
438+
fn initialize(
439+
(): (),
440+
_jobs: &mut JobQueue<Self::Jobs>,
441+
) -> Result<Vec<AlphaEvent>, AlphaError> {
440442
Ok(vec![AlphaEvent::Born])
441443
}
442444

443-
async fn transition(&self, (): (), (): &()) -> Result<Vec<AlphaEvent>, AlphaError> {
445+
fn transition(
446+
&self,
447+
(): (),
448+
_jobs: &mut JobQueue<Self::Jobs>,
449+
) -> Result<Vec<AlphaEvent>, AlphaError> {
444450
Ok(vec![])
445451
}
446452
}
@@ -468,13 +474,12 @@ mod tests {
468474
#[error("beta error")]
469475
struct BetaError;
470476

471-
#[async_trait]
472477
impl EventSourced for Beta {
473478
type Id = String;
474479
type Event = BetaEvent;
475480
type Command = ();
476481
type Error = BetaError;
477-
type Services = ();
482+
type Jobs = Nil;
478483
type Materialized = Nil;
479484

480485
const AGGREGATE_TYPE: &'static str = "Beta";
@@ -491,11 +496,18 @@ mod tests {
491496
Ok(None)
492497
}
493498

494-
async fn initialize((): (), (): &()) -> Result<Vec<BetaEvent>, BetaError> {
499+
fn initialize(
500+
(): (),
501+
_jobs: &mut JobQueue<Self::Jobs>,
502+
) -> Result<Vec<BetaEvent>, BetaError> {
495503
Ok(vec![BetaEvent::Spawned])
496504
}
497505

498-
async fn transition(&self, (): (), (): &()) -> Result<Vec<BetaEvent>, BetaError> {
506+
fn transition(
507+
&self,
508+
(): (),
509+
_jobs: &mut JobQueue<Self::Jobs>,
510+
) -> Result<Vec<BetaEvent>, BetaError> {
499511
Ok(vec![])
500512
}
501513
}

crates/event-sorcery/src/job.rs

Lines changed: 78 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ use std::future::Future;
3030
use std::marker::PhantomData;
3131
use std::sync::Arc;
3232
use std::time::Duration;
33-
use tracing::{debug, error};
33+
use tracing::{debug, error, warn};
3434

3535
use 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

9396
impl 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+
498564
fn build_poll_config<J: Job>() -> Config {
499565
let strategy = StrategyBuilder::new()
500566
.apply(

0 commit comments

Comments
 (0)