use async_trait::async_trait;
use futures::{FutureExt, StreamExt};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use std::sync::Arc;
use job::{
CurrentJob, Job, JobCompletion, JobInitializer, JobRunner, JobSpawner, JobType, RetrySettings,
};
use super::ctx::*;
use super::{EphemeralOutboxListener, Outbox, event::*};
use crate::tables::MailboxTables;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EventSubscription {
All,
PersistentOnly,
EphemeralOnly,
}
pub trait OutboxEventHandler<P>: Send + Sync + 'static
where
P: Serialize + DeserializeOwned + Send + Sync + 'static + Unpin,
{
const SUBSCRIPTION: EventSubscription = EventSubscription::All;
type Batch: Default + Send + 'static;
fn handle_persistent<'inv>(
&self,
ctx: EventCtx<'inv, Self::Batch>,
event: &PersistentOutboxEvent<P>,
) -> impl std::future::Future<
Output = Result<Handled<'inv>, Box<dyn std::error::Error + Send + Sync>>,
> + Send {
let _ = event;
async move { Ok(ctx.skip()) }
}
fn flush(
&self,
op: &mut FlushOp<'_>,
items: Self::Batch,
) -> impl std::future::Future<Output = Result<(), Box<dyn std::error::Error + Send + Sync>>> + Send
{
let _ = (op, items);
async { Ok(()) }
}
fn handle_ephemeral(
&self,
event: &EphemeralOutboxEvent<P>,
) -> impl std::future::Future<Output = Result<(), Box<dyn std::error::Error + Send + Sync>>> + Send
{
let _ = event;
async { Ok(()) }
}
}
struct HandlerFlusher<H, P> {
handler: Arc<H>,
_payload: std::marker::PhantomData<fn() -> P>,
}
impl<H, P> ItemFlush<H::Batch> for HandlerFlusher<H, P>
where
H: OutboxEventHandler<P>,
P: Serialize + DeserializeOwned + Send + Sync + 'static + Unpin,
{
fn flush_items<'a>(
&'a self,
op: &'a mut es_entity::DbOp<'static>,
items: H::Batch,
) -> BoxFuture<'a, Result<(), HandlerError>> {
Box::pin(async move {
let mut op = FlushOp::new(op);
self.handler.flush(&mut op, items).await
})
}
}
enum NextDelivery<P>
where
P: Serialize + DeserializeOwned + Send + Sync + 'static,
{
Persistent(Option<Arc<PersistentOutboxEvent<P>>>),
Ephemeral(Arc<EphemeralOutboxEvent<P>>),
}
async fn next_if_subscribed<P>(
listener: &mut Option<EphemeralOutboxListener<P>>,
) -> Option<Arc<EphemeralOutboxEvent<P>>>
where
P: Serialize + DeserializeOwned + Send + Sync + 'static + Unpin,
{
match listener {
Some(listener) => listener.next().await,
None => std::future::pending().await,
}
}
const DEFAULT_MAX_BATCH_SIZE: usize = 100;
const DEFAULT_CHECKPOINT_INTERVAL: std::time::Duration = std::time::Duration::from_secs(5);
#[derive(Clone)]
pub struct OutboxEventJobConfig {
pub job_type: JobType,
pub retry_settings: RetrySettings,
pub max_batch_size: usize,
pub checkpoint_interval: std::time::Duration,
}
impl OutboxEventJobConfig {
pub fn new(job_type: JobType) -> Self {
Self {
job_type,
retry_settings: RetrySettings::repeat_indefinitely(),
max_batch_size: DEFAULT_MAX_BATCH_SIZE,
checkpoint_interval: DEFAULT_CHECKPOINT_INTERVAL,
}
}
pub fn with_retry_settings(mut self, settings: RetrySettings) -> Self {
self.retry_settings = settings;
self
}
pub fn with_max_batch_size(mut self, max_batch_size: usize) -> Self {
self.max_batch_size = max_batch_size.max(1);
self
}
pub fn with_checkpoint_interval(mut self, interval: std::time::Duration) -> Self {
self.checkpoint_interval = interval;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub(super) struct OutboxEventJobData {}
pub(super) struct OutboxEventJobInitializer<H, P, Tables>
where
H: OutboxEventHandler<P>,
P: Serialize + DeserializeOwned + Send + Sync + 'static + Unpin,
Tables: MailboxTables,
{
outbox: Outbox<P, Tables>,
handler: Arc<H>,
job_type: JobType,
retry_settings: RetrySettings,
max_batch_size: usize,
checkpoint_interval: std::time::Duration,
}
impl<H, P, Tables> OutboxEventJobInitializer<H, P, Tables>
where
H: OutboxEventHandler<P>,
P: Serialize + DeserializeOwned + Send + Sync + 'static + Unpin,
Tables: MailboxTables,
{
pub fn new(outbox: Outbox<P, Tables>, handler: H, config: &OutboxEventJobConfig) -> Self {
Self {
outbox,
handler: Arc::new(handler),
job_type: config.job_type.clone(),
retry_settings: config.retry_settings.clone(),
max_batch_size: config.max_batch_size,
checkpoint_interval: config.checkpoint_interval,
}
}
}
impl<H, P, Tables> JobInitializer for OutboxEventJobInitializer<H, P, Tables>
where
H: OutboxEventHandler<P>,
P: Serialize + DeserializeOwned + Send + Sync + 'static + Unpin,
Tables: MailboxTables,
{
type Config = OutboxEventJobData;
fn job_type(&self) -> JobType {
self.job_type.clone()
}
fn retry_on_error_settings(&self) -> RetrySettings {
self.retry_settings.clone()
}
fn init(
&self,
_job: &Job,
_: JobSpawner<Self::Config>,
) -> Result<Box<dyn JobRunner>, Box<dyn std::error::Error>> {
Ok(Box::new(OutboxEventJobRunner::<H, P, Tables> {
outbox: self.outbox.clone(),
handler: self.handler.clone(),
max_batch_size: self.max_batch_size,
checkpoint_interval: self.checkpoint_interval,
}))
}
}
struct OutboxEventJobRunner<H, P, Tables>
where
H: OutboxEventHandler<P>,
P: Serialize + DeserializeOwned + Send + Sync + 'static + Unpin,
Tables: MailboxTables,
{
outbox: Outbox<P, Tables>,
handler: Arc<H>,
max_batch_size: usize,
checkpoint_interval: std::time::Duration,
}
#[async_trait]
impl<H, P, Tables> JobRunner for OutboxEventJobRunner<H, P, Tables>
where
H: OutboxEventHandler<P>,
P: Serialize + DeserializeOwned + Send + Sync + 'static + Unpin,
Tables: MailboxTables,
{
async fn run(
&self,
current_job: CurrentJob,
) -> Result<JobCompletion, Box<dyn std::error::Error>> {
match H::SUBSCRIPTION {
EventSubscription::EphemeralOnly => self.run_ephemeral_only(current_job).await,
EventSubscription::All | EventSubscription::PersistentOnly => {
self.run_with_persistent(current_job).await
}
}
}
}
impl<H, P, Tables> OutboxEventJobRunner<H, P, Tables>
where
H: OutboxEventHandler<P>,
P: Serialize + DeserializeOwned + Send + Sync + 'static + Unpin,
Tables: MailboxTables,
{
async fn run_ephemeral_only(
&self,
mut current_job: CurrentJob,
) -> Result<JobCompletion, Box<dyn std::error::Error>> {
let mut ephemeral = self.outbox.listen_ephemeral();
loop {
tokio::select! {
biased;
_ = current_job.shutdown_requested() => {
return Ok(JobCompletion::RescheduleNow);
}
event = ephemeral.next() => match event {
Some(event) => {
self.handler
.handle_ephemeral(&event)
.await
.map_err(|e| e as Box<dyn std::error::Error>)?;
}
None => return Ok(JobCompletion::RescheduleNow),
},
}
}
}
async fn run_with_persistent(
&self,
mut current_job: CurrentJob,
) -> Result<JobCompletion, Box<dyn std::error::Error>> {
let mut state = current_job
.execution_state::<OutboxEventJobState>()?
.unwrap_or_default();
let mut persistent = self.outbox.listen_persisted(Some(state.sequence));
let mut ephemeral =
(H::SUBSCRIPTION == EventSubscription::All).then(|| self.outbox.listen_ephemeral());
let mut op_slot: Option<es_entity::DbOp<'static>> = None;
let mut tracker = BatchTracker {
events_in_op: 0,
collected: 0,
persisted_seq: state.sequence,
last_persist: tokio::time::Instant::now(),
};
let mut batch = H::Batch::default();
let flusher = HandlerFlusher::<H, P> {
handler: self.handler.clone(),
_payload: std::marker::PhantomData,
};
loop {
let event = if op_slot.is_some() || tracker.collected > 0 {
match persistent.next().now_or_never() {
Some(Some(event)) => event,
Some(None) => {
let mut parts = CtxParts {
op_slot: &mut op_slot,
current_job: &mut current_job,
state: &state,
tracker: &mut tracker,
};
flush_batch(&mut parts, &mut batch, &flusher, "stream_closed")
.await
.map_err(|e| e as Box<dyn std::error::Error>)?;
return Ok(JobCompletion::RescheduleNow);
}
None => {
let mut parts = CtxParts {
op_slot: &mut op_slot,
current_job: &mut current_job,
state: &state,
tracker: &mut tracker,
};
flush_batch(&mut parts, &mut batch, &flusher, "backlog_drained")
.await
.map_err(|e| e as Box<dyn std::error::Error>)?;
continue;
}
}
} else {
let next = tokio::select! {
biased;
_ = current_job.shutdown_requested() => {
if tracker.persisted_seq < state.sequence {
persist_checkpoint(&mut current_job, &state)
.await
.map_err(|e| e as Box<dyn std::error::Error>)?;
}
return Ok(JobCompletion::RescheduleNow);
}
_ = tokio::time::sleep_until(tracker.last_persist + self.checkpoint_interval),
if tracker.persisted_seq < state.sequence => {
persist_checkpoint(&mut current_job, &state)
.await
.map_err(|e| e as Box<dyn std::error::Error>)?;
tracker.persisted_seq = state.sequence;
tracker.last_persist = tokio::time::Instant::now();
continue;
}
next = async {
tokio::select! {
Some(event) = next_if_subscribed(&mut ephemeral) => {
NextDelivery::Ephemeral(event)
}
event = persistent.next() => NextDelivery::Persistent(event),
}
} => next,
};
match next {
NextDelivery::Ephemeral(event) => {
self.handler
.handle_ephemeral(&event)
.await
.map_err(|e| e as Box<dyn std::error::Error>)?;
continue;
}
NextDelivery::Persistent(Some(event)) => event,
NextDelivery::Persistent(None) => {
if tracker.persisted_seq < state.sequence {
persist_checkpoint(&mut current_job, &state)
.await
.map_err(|e| e as Box<dyn std::error::Error>)?;
}
return Ok(JobCompletion::RescheduleNow);
}
}
};
let ctx = EventCtx {
parts: CtxParts {
op_slot: &mut op_slot,
current_job: &mut current_job,
state: &state,
tracker: &mut tracker,
},
batch: &mut batch,
flusher: &flusher,
};
let outcome = self
.handler
.handle_persistent(ctx, &event)
.await
.map_err(|e| e as Box<dyn std::error::Error>)?
.outcome;
state.sequence = event.sequence;
match outcome {
Outcome::Skip => {}
Outcome::Commit => {
let mut parts = CtxParts {
op_slot: &mut op_slot,
current_job: &mut current_job,
state: &state,
tracker: &mut tracker,
};
flush_batch(&mut parts, &mut batch, &flusher, "commit")
.await
.map_err(|e| e as Box<dyn std::error::Error>)?;
}
Outcome::Defer | Outcome::Collect => {
if tracker.events_in_op >= self.max_batch_size {
let mut parts = CtxParts {
op_slot: &mut op_slot,
current_job: &mut current_job,
state: &state,
tracker: &mut tracker,
};
flush_batch(&mut parts, &mut batch, &flusher, "batch_full")
.await
.map_err(|e| e as Box<dyn std::error::Error>)?;
}
}
}
}
}
}