mod runner;
mod waker;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use std::{marker::PhantomData, time::Duration};
use job::JobType;
use crate::out::Subscription;
use crate::out::ctx::{FlushOp, Handled, KeyedEventCtx};
use crate::out::event::{EventDelivery, PersistentOutboxEvent, UndecodableDelivery};
use crate::out::lane::InsertOrder;
use crate::tables::MailboxTables;
pub(in crate::out) use runner::KeyedSubscriberJobInitializer;
pub(in crate::out) use waker::{WakeRoutes, wake_route, waker_handler, waker_job_type};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct WakeKey(pub(crate) String);
impl WakeKey {
pub fn as_str(&self) -> &str {
&self.0
}
}
impl From<String> for WakeKey {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for WakeKey {
fn from(s: &str) -> Self {
Self(s.to_string())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WakeKeys(Vec<WakeKey>);
impl WakeKeys {
pub fn new(first: WakeKey) -> Self {
Self(vec![first])
}
pub fn and(mut self, key: WakeKey) -> Self {
self.0.push(key);
self
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
false
}
pub fn iter(&self) -> impl Iterator<Item = &WakeKey> {
self.0.iter()
}
pub(crate) fn into_strings(self) -> Vec<String> {
self.0.into_iter().map(|k| k.0).collect()
}
}
impl From<WakeKey> for WakeKeys {
fn from(key: WakeKey) -> Self {
Self::new(key)
}
}
impl TryFrom<Vec<WakeKey>> for WakeKeys {
type Error = SubscribeError;
fn try_from(keys: Vec<WakeKey>) -> Result<Self, Self::Error> {
if keys.is_empty() {
return Err(SubscribeError::EmptyWakeKeys);
}
Ok(Self(keys))
}
}
impl IntoIterator for WakeKeys {
type Item = WakeKey;
type IntoIter = std::vec::IntoIter<WakeKey>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl std::fmt::Display for WakeKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
pub trait KeyedSubscriber<P>: Send + Sync + 'static
where
P: Serialize + DeserializeOwned + Send + Sync + 'static + Unpin,
{
type Batch: Default + Send + 'static;
fn handle<'inv>(
&self,
ctx: KeyedEventCtx<'inv, Self::Batch>,
event: &EventDelivery<P>,
) -> impl std::future::Future<
Output = Result<Handled<'inv>, Box<dyn std::error::Error + Send + Sync>>,
> + Send;
fn handle_undecodable(
&self,
error: &UndecodableDelivery,
) -> impl std::future::Future<Output = Result<(), Box<dyn std::error::Error + Send + Sync>>> + Send
{
let error = error.inner().clone();
async move { Err(error.into()) }
}
fn flush(
&self,
op: &mut FlushOp<'_, InsertOrder>,
items: Self::Batch,
) -> impl std::future::Future<Output = Result<(), Box<dyn std::error::Error + Send + Sync>>> + Send
{
let _ = (op, items);
async { Ok(()) }
}
}
pub trait SubscriptionDef<P>: Send + Sync + 'static
where
P: Serialize + DeserializeOwned + Send + Sync + 'static + Unpin,
{
type Key: Serialize
+ DeserializeOwned
+ std::fmt::Display
+ std::str::FromStr
+ Clone
+ Send
+ Sync
+ 'static;
type InstanceConfig: Serialize + DeserializeOwned + Send + Sync + 'static;
type Subscriber: KeyedSubscriber<P>;
fn wake_keys(&self, event: &PersistentOutboxEvent<P>) -> impl IntoIterator<Item = WakeKey>;
fn instantiate(&self, key: Self::Key, cfg: Self::InstanceConfig) -> Self::Subscriber;
}
#[derive(Debug, thiserror::Error)]
pub enum SubscribeError {
#[error("SubscribeError - EmptyWakeKeys: a subscription must declare at least one wake key")]
EmptyWakeKeys,
}
const DEFAULT_LINGER: Duration = Duration::from_secs(30);
const DEFAULT_MAX_BATCH_SIZE: usize = 100;
const DEFAULT_CHECKPOINT_INTERVAL: Duration = Duration::from_secs(5);
#[derive(Clone)]
pub struct KeyedSubscriberConfig {
pub job_type: JobType,
pub linger: Duration,
pub checkpoint_interval: Duration,
pub max_batch_size: usize,
pub max_concurrent_per_process: Option<usize>,
}
impl KeyedSubscriberConfig {
pub fn new(job_type: JobType) -> Self {
Self {
job_type,
linger: DEFAULT_LINGER,
checkpoint_interval: DEFAULT_CHECKPOINT_INTERVAL,
max_batch_size: DEFAULT_MAX_BATCH_SIZE,
max_concurrent_per_process: None,
}
}
pub fn with_linger(mut self, linger: Duration) -> Self {
self.linger = linger;
self
}
pub fn with_checkpoint_interval(mut self, interval: Duration) -> Self {
self.checkpoint_interval = interval;
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_max_concurrent_per_process(mut self, n: usize) -> Self {
self.max_concurrent_per_process = Some(n);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(in crate::out) struct KeyMsg {
key: String,
}
pub struct Subscriptions<D, P, Tables = crate::tables::DefaultMailboxTables>
where
D: SubscriptionDef<P>,
P: Serialize + DeserializeOwned + Send + Sync + 'static + Unpin,
{
pool: sqlx::PgPool,
clock: es_entity::clock::ClockHandle,
job_type: JobType,
jobs: job::Jobs,
spawner: job::KeyedJobSpawner<KeyMsg>,
_phantom: PhantomData<(D, P, Tables)>,
}
impl<D, P, Tables> Clone for Subscriptions<D, P, Tables>
where
D: SubscriptionDef<P>,
P: Serialize + DeserializeOwned + Send + Sync + 'static + Unpin,
Tables: MailboxTables,
{
fn clone(&self) -> Self {
Self {
pool: self.pool.clone(),
clock: self.clock.clone(),
job_type: self.job_type.clone(),
jobs: self.jobs.clone(),
spawner: self.spawner.clone(),
_phantom: PhantomData,
}
}
}
impl<D, P, Tables> Subscriptions<D, P, Tables>
where
D: SubscriptionDef<P>,
P: Serialize + DeserializeOwned + Send + Sync + 'static + Unpin,
Tables: MailboxTables,
{
pub(in crate::out) fn new(
pool: sqlx::PgPool,
clock: es_entity::clock::ClockHandle,
job_type: JobType,
jobs: job::Jobs,
spawner: job::KeyedJobSpawner<KeyMsg>,
) -> Self {
Self {
pool,
clock,
job_type,
jobs,
spawner,
_phantom: PhantomData,
}
}
#[tracing::instrument(name = "obix.subscriptions.subscribe_in_op", skip_all, err)]
pub async fn subscribe_in_op(
&self,
op: &mut impl es_entity::AtomicOperation,
key: D::Key,
cfg: D::InstanceConfig,
wake_keys: impl Into<WakeKeys>,
) -> Result<Subscription<P, InsertOrder, Tables>, Box<dyn std::error::Error + Send + Sync>>
{
let key_str = key.to_string();
let wake_keys = wake_keys.into().into_strings();
let instance_config = serde_json::to_value(&cfg)?;
let start_after = {
let fut: std::pin::Pin<
Box<
dyn std::future::Future<Output = Result<crate::EventSequence, sqlx::Error>>
+ Send
+ '_,
>,
> = Box::pin(Tables::highest_known_persistent_sequence(&mut *op));
fut.await?
};
Tables::insert_subscription_in_op(
op,
self.job_type.as_str(),
&key_str,
&wake_keys,
instance_config,
start_after,
)
.await?;
self.spawner
.spawn_in_op(
op,
key_str.clone(),
KeyMsg {
key: key_str.clone(),
},
)
.await?;
Ok(Subscription::new_keyed(
self.jobs.clone(),
self.job_type.clone(),
key_str,
self.pool.clone(),
))
}
#[tracing::instrument(name = "obix.subscriptions.cancel_in_op", skip_all, err)]
pub async fn cancel_in_op(
&self,
op: &mut impl es_entity::AtomicOperation,
key: &D::Key,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
Tables::delete_subscription_in_op(op, self.job_type.as_str(), &key.to_string()).await?;
Ok(())
}
#[tracing::instrument(name = "obix.subscriptions.cancel", skip_all, err)]
pub async fn cancel(
&self,
key: &D::Key,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut op = es_entity::DbOp::init_with_clock(&self.pool, &self.clock).await?;
self.cancel_in_op(&mut op, key).await?;
op.commit().await?;
Ok(())
}
#[tracing::instrument(name = "obix.subscriptions.subscription", skip_all, err)]
pub async fn subscription(
&self,
key: &D::Key,
) -> Result<Subscription<P, InsertOrder, Tables>, Box<dyn std::error::Error + Send + Sync>>
{
let key_str = key.to_string();
self.jobs
.keyed_handle(self.job_type.clone(), key_str.clone())
.await?
.ok_or("no subscription has ever existed for this key")?;
Ok(Subscription::new_keyed(
self.jobs.clone(),
self.job_type.clone(),
key_str,
self.pool.clone(),
))
}
}