use serde::{Deserialize, Serialize};
use std::marker::PhantomData;
use job::CurrentJob;
use crate::sequence::EventSequence;
pub(crate) type HandlerError = Box<dyn std::error::Error + Send + Sync>;
#[derive(Default, Clone, Copy, Serialize, Deserialize)]
pub(crate) struct OutboxEventJobState {
pub(crate) sequence: EventSequence,
}
pub(crate) struct BatchTracker {
pub(crate) events_in_op: usize,
pub(crate) persisted_seq: EventSequence,
pub(crate) last_persist: tokio::time::Instant,
}
pub(crate) struct CtxParts<'inv> {
pub(crate) op_slot: &'inv mut Option<es_entity::DbOp<'static>>,
pub(crate) current_job: &'inv mut CurrentJob,
pub(crate) state: &'inv OutboxEventJobState,
pub(crate) tracker: &'inv mut BatchTracker,
}
#[must_use = "return the Handled token from handle_persistent"]
pub struct Handled<'inv> {
pub(crate) outcome: Outcome,
pub(crate) _invocation: PhantomData<&'inv ()>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Outcome {
Skip,
Commit,
Defer,
}
#[must_use = "resolve the EventCtx via skip / consume_in_batch / consume_isolated"]
pub struct EventCtx<'inv> {
pub(crate) parts: CtxParts<'inv>,
}
impl<'inv> EventCtx<'inv> {
pub fn skip(self) -> Handled<'inv> {
Handled {
outcome: Outcome::Skip,
_invocation: PhantomData,
}
}
pub async fn consume_in_batch(self) -> Result<BatchOp<'inv>, HandlerError> {
let parts = self.parts;
if parts.op_slot.is_none() {
*parts.op_slot = Some(
es_entity::DbOp::init_with_clock(
parts.current_job.pool(),
parts.current_job.clock(),
)
.await?,
);
}
parts.tracker.events_in_op += 1;
Ok(BatchOp { parts })
}
pub async fn consume_isolated(self) -> Result<IsolatedOp<'inv>, HandlerError> {
let mut parts = self.parts;
flush_batch(&mut parts, "isolated_entry").await?;
*parts.op_slot = Some(
es_entity::DbOp::init_with_clock(parts.current_job.pool(), parts.current_job.clock())
.await?,
);
parts.tracker.events_in_op = 1;
Ok(IsolatedOp { parts })
}
}
#[must_use = "exit with .commit() or .defer() to produce the Handled token"]
pub struct BatchOp<'inv> {
parts: CtxParts<'inv>,
}
impl<'inv> BatchOp<'inv> {
pub fn commit(self) -> Handled<'inv> {
Handled {
outcome: Outcome::Commit,
_invocation: PhantomData,
}
}
pub fn defer(self) -> Handled<'inv> {
Handled {
outcome: Outcome::Defer,
_invocation: PhantomData,
}
}
}
impl std::ops::Deref for BatchOp<'_> {
type Target = es_entity::DbOp<'static>;
fn deref(&self) -> &Self::Target {
self.parts
.op_slot
.as_ref()
.expect("BatchOp always holds a materialized op")
}
}
impl std::ops::DerefMut for BatchOp<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.parts
.op_slot
.as_mut()
.expect("BatchOp always holds a materialized op")
}
}
impl es_entity::AtomicOperation for BatchOp<'_> {
fn maybe_now(&self) -> Option<chrono::DateTime<chrono::Utc>> {
(**self).maybe_now()
}
fn clock(&self) -> &es_entity::clock::ClockHandle {
es_entity::AtomicOperation::clock(&**self)
}
fn connection(&mut self) -> &mut es_entity::db::Connection {
(**self).connection()
}
fn as_executor(&mut self) -> es_entity::OneTimeExecutor<'_, &mut es_entity::db::Connection> {
(**self).as_executor()
}
fn add_commit_hook<H: es_entity::hooks::CommitHook>(&mut self, hook: H) -> Result<(), H> {
(**self).add_commit_hook(hook)
}
fn commit_hook<H: es_entity::hooks::CommitHook>(&self) -> Option<&H> {
(**self).commit_hook::<H>()
}
fn supports_hooks(&self) -> bool {
(**self).supports_hooks()
}
}
#[must_use = "exit with .commit() to produce the Handled token"]
pub struct IsolatedOp<'inv> {
parts: CtxParts<'inv>,
}
impl<'inv> IsolatedOp<'inv> {
pub fn commit(self) -> Handled<'inv> {
Handled {
outcome: Outcome::Commit,
_invocation: PhantomData,
}
}
}
impl std::ops::Deref for IsolatedOp<'_> {
type Target = es_entity::DbOp<'static>;
fn deref(&self) -> &Self::Target {
self.parts
.op_slot
.as_ref()
.expect("IsolatedOp always holds a materialized op")
}
}
impl std::ops::DerefMut for IsolatedOp<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.parts
.op_slot
.as_mut()
.expect("IsolatedOp always holds a materialized op")
}
}
impl es_entity::AtomicOperation for IsolatedOp<'_> {
fn maybe_now(&self) -> Option<chrono::DateTime<chrono::Utc>> {
(**self).maybe_now()
}
fn clock(&self) -> &es_entity::clock::ClockHandle {
es_entity::AtomicOperation::clock(&**self)
}
fn connection(&mut self) -> &mut es_entity::db::Connection {
(**self).connection()
}
fn as_executor(&mut self) -> es_entity::OneTimeExecutor<'_, &mut es_entity::db::Connection> {
(**self).as_executor()
}
fn add_commit_hook<H: es_entity::hooks::CommitHook>(&mut self, hook: H) -> Result<(), H> {
(**self).add_commit_hook(hook)
}
fn commit_hook<H: es_entity::hooks::CommitHook>(&self) -> Option<&H> {
(**self).commit_hook::<H>()
}
fn supports_hooks(&self) -> bool {
(**self).supports_hooks()
}
}
#[tracing::instrument(
name = "outbox.flush_batch",
skip_all,
fields(
reason = reason,
batch_size = parts.tracker.events_in_op,
checkpoint_seq = u64::from(parts.state.sequence),
),
err
)]
pub(crate) async fn flush_batch(
parts: &mut CtxParts<'_>,
reason: &'static str,
) -> Result<(), HandlerError> {
let Some(mut op) = parts.op_slot.take() else {
return Ok(());
};
parts
.current_job
.update_execution_state_in_op(&mut op, parts.state)
.await?;
op.commit().await?;
parts.tracker.persisted_seq = parts.state.sequence;
parts.tracker.last_persist = tokio::time::Instant::now();
parts.tracker.events_in_op = 0;
Ok(())
}
#[tracing::instrument(
name = "outbox.persist_checkpoint",
skip_all,
fields(checkpoint_seq = u64::from(state.sequence)),
err
)]
pub(crate) async fn persist_checkpoint(
current_job: &mut CurrentJob,
state: &OutboxEventJobState,
) -> Result<(), HandlerError> {
let mut op = es_entity::DbOp::init_with_clock(current_job.pool(), current_job.clock()).await?;
current_job
.update_execution_state_in_op(&mut op, state)
.await?;
op.commit().await?;
Ok(())
}