use serde::{Serialize, de::DeserializeOwned};
use std::collections::{HashMap, HashSet};
use std::sync::{Arc, RwLock};
use job::JobType;
use super::{KeyMsg, SubscriptionDef, WakeKey};
use es_entity::AtomicOperation as _;
use crate::out::StreamSelection;
use crate::out::ctx::{EventCtx, FlushOp, Handled};
use crate::out::event::{EventDelivery, PersistentOutboxEvent};
use crate::out::lane::InsertOrder;
use crate::out::subscription::singleton::SingletonSubscriber;
use crate::sequence::EventSequence;
use crate::tables::MailboxTables;
pub(in crate::out) trait WakeRoute<P>: Send + Sync + 'static
where
P: Serialize + DeserializeOwned + Send + Sync + 'static + Unpin,
{
fn subscriber_type(&self) -> &str;
fn wake_keys(&self, event: &PersistentOutboxEvent<P>) -> Vec<WakeKey>;
fn spawner(&self) -> &job::KeyedJobSpawner<KeyMsg>;
}
struct TypedWakeRoute<D, P>
where
D: SubscriptionDef<P>,
P: Serialize + DeserializeOwned + Send + Sync + 'static + Unpin,
{
def: Arc<D>,
subscriber_type: JobType,
spawner: job::KeyedJobSpawner<KeyMsg>,
_marker: std::marker::PhantomData<fn() -> P>,
}
impl<D, P> WakeRoute<P> for TypedWakeRoute<D, P>
where
D: SubscriptionDef<P>,
P: Serialize + DeserializeOwned + Send + Sync + 'static + Unpin,
{
fn subscriber_type(&self) -> &str {
self.subscriber_type.as_str()
}
fn wake_keys(&self, event: &PersistentOutboxEvent<P>) -> Vec<WakeKey> {
self.def.wake_keys(event).into_iter().collect()
}
fn spawner(&self) -> &job::KeyedJobSpawner<KeyMsg> {
&self.spawner
}
}
pub(in crate::out) type WakeRoutes<P> = Arc<RwLock<Vec<Arc<dyn WakeRoute<P>>>>>;
pub(in crate::out) fn wake_route<D, P>(
def: Arc<D>,
subscriber_type: JobType,
spawner: job::KeyedJobSpawner<KeyMsg>,
) -> Arc<dyn WakeRoute<P>>
where
D: SubscriptionDef<P>,
P: Serialize + DeserializeOwned + Send + Sync + 'static + Unpin,
{
Arc::new(TypedWakeRoute {
def,
subscriber_type,
spawner,
_marker: std::marker::PhantomData,
})
}
const CATCH_UP_WAKE_LIMIT: i64 = 64;
const CATCH_UP_TRIGGER_NUMERATOR: u64 = 3;
const CATCH_UP_TRIGGER_DENOMINATOR: u64 = 4;
pub(in crate::out) struct WakerHandler<P, Tables>
where
P: Serialize + DeserializeOwned + Send + Sync + 'static + Unpin,
Tables: MailboxTables,
{
routes: WakeRoutes<P>,
catch_up_lag: u64,
catch_up_stride: u64,
last_catch_up: Arc<std::sync::atomic::AtomicU64>,
_marker: std::marker::PhantomData<fn() -> Tables>,
}
pub(in crate::out) fn waker_job_type<Tables: MailboxTables>() -> JobType {
JobType::new(Tables::KEYED_WAKER_JOB_TYPE)
}
pub(in crate::out) fn waker_handler<P, Tables>(
routes: WakeRoutes<P>,
event_cache_size: usize,
) -> WakerHandler<P, Tables>
where
P: Serialize + DeserializeOwned + Send + Sync + 'static + Unpin,
Tables: MailboxTables,
{
let cache_size = event_cache_size as u64;
WakerHandler {
routes,
catch_up_lag: (cache_size * CATCH_UP_TRIGGER_NUMERATOR / CATCH_UP_TRIGGER_DENOMINATOR)
.max(1),
catch_up_stride: (cache_size / CATCH_UP_TRIGGER_DENOMINATOR).max(1),
last_catch_up: Arc::new(std::sync::atomic::AtomicU64::new(0)),
_marker: std::marker::PhantomData,
}
}
impl<P, Tables> SingletonSubscriber<P> for WakerHandler<P, Tables>
where
P: Serialize + DeserializeOwned + Send + Sync + 'static + Unpin,
Tables: MailboxTables,
{
const SUBSCRIPTION: StreamSelection = StreamSelection::PersistentOnly;
type Batch = WakeBatch;
async fn handle_persistent<'inv>(
&self,
ctx: EventCtx<'inv, Self::Batch>,
event: &EventDelivery<P>,
) -> Result<Handled<'inv>, Box<dyn std::error::Error + Send + Sync>> {
let matched: Vec<(usize, Vec<WakeKey>)> = {
let routes = self.routes.read().expect("wake routes poisoned");
routes
.iter()
.enumerate()
.filter_map(|(idx, route)| {
let keys = route.wake_keys(event);
(!keys.is_empty()).then_some((idx, keys))
})
.collect()
};
let catch_up_head = self.catch_up_due(event.sequence).then_some(event.sequence);
if matched.is_empty() && catch_up_head.is_none() {
return Ok(ctx.skip());
}
Ok(ctx.collect_with(move |batch| {
for (idx, keys) in matched {
batch.per_route.entry(idx).or_default().extend(keys);
}
if let Some(head) = catch_up_head {
batch.catch_up_head = Some(
batch
.catch_up_head
.map_or(head, |current| current.max(head)),
);
}
}))
}
async fn flush(
&self,
op: &mut FlushOp<'_, InsertOrder>,
items: Self::Batch,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let WakeBatch {
per_route,
catch_up_head,
} = items;
if per_route.is_empty() && catch_up_head.is_none() {
return Ok(());
}
let routes: Vec<Arc<dyn WakeRoute<P>>> =
self.routes.read().expect("wake routes poisoned").clone();
let route_idx: HashMap<&str, usize> = routes
.iter()
.enumerate()
.map(|(idx, r)| (r.subscriber_type(), idx))
.collect();
let mut to_wake: HashMap<usize, HashMap<String, bool>> = HashMap::new();
let (types, wake_keys): (Vec<String>, Vec<String>) = per_route
.into_iter()
.filter_map(|(idx, keys)| routes.get(idx).map(|route| (route, keys)))
.flat_map(|(route, keys)| {
keys.into_iter()
.map(|key| (route.subscriber_type().to_string(), key.0))
})
.unzip();
for (subscriber_type, key) in Tables::subscriptions_for_wake_keys(op, &types, &wake_keys)
.await?
.into_iter()
{
if let Some(idx) = route_idx.get(subscriber_type.as_str()) {
to_wake.entry(*idx).or_default().insert(key, true);
}
}
if let Some(head) = catch_up_head {
let below = EventSequence::from(u64::from(head).saturating_sub(self.catch_up_lag));
let registered: Vec<String> = routes
.iter()
.map(|r| r.subscriber_type().to_string())
.collect();
let behind =
Tables::subscriptions_behind(op, ®istered, below, CATCH_UP_WAKE_LIMIT).await?;
for (subscriber_type, key) in behind {
if let Some(idx) = route_idx.get(subscriber_type.as_str()) {
to_wake.entry(*idx).or_default().entry(key).or_insert(false);
}
}
let _ = op.add_commit_hook(CatchUpClaimed {
cell: self.last_catch_up.clone(),
head: u64::from(head),
});
}
for (idx, keys) in to_wake {
let Some(route) = routes.get(idx) else {
continue;
};
self.spawn_all(op, route, keys.into_iter().collect())
.await?;
}
Ok(())
}
}
#[derive(Default)]
pub(in crate::out) struct WakeBatch {
per_route: HashMap<usize, HashSet<WakeKey>>,
catch_up_head: Option<EventSequence>,
}
struct CatchUpClaimed {
cell: Arc<std::sync::atomic::AtomicU64>,
head: u64,
}
impl es_entity::hooks::CommitHook for CatchUpClaimed {
fn post_commit(self) {
self.cell
.fetch_max(self.head, std::sync::atomic::Ordering::Relaxed);
}
}
impl<P, Tables> WakerHandler<P, Tables>
where
P: Serialize + DeserializeOwned + Send + Sync + 'static + Unpin,
Tables: MailboxTables,
{
fn catch_up_due(&self, head: EventSequence) -> bool {
use std::sync::atomic::Ordering;
let head = u64::from(head);
if head.checked_sub(self.catch_up_lag).is_none() {
return false;
}
let last = self.last_catch_up.load(Ordering::Relaxed);
head >= last.saturating_add(self.catch_up_stride)
}
async fn spawn_all(
&self,
op: &mut FlushOp<'_, InsertOrder>,
route: &Arc<dyn WakeRoute<P>>,
keys: Vec<(String, bool)>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if keys.is_empty() {
return Ok(());
}
let specs = keys
.into_iter()
.map(|(key, pull_forward)| {
let spec = job::KeyedJobSpec::new(key.clone(), KeyMsg { key });
if pull_forward {
spec.force_reschedule()
} else {
spec
}
})
.collect();
route.spawner().spawn_all_in_op(op, specs).await?;
Ok(())
}
}