use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::time::Duration;
use eventcore_types::{BatchSize, ProjectionSelection};
use thiserror::Error;
use tokio_util::sync::CancellationToken;
pub trait ProjectionRetrySleeper: std::fmt::Debug + Send + Sync {
fn sleep(&self, duration: Duration) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>;
}
pub trait ProjectionPollSleeper: std::fmt::Debug + Send + Sync {
fn sleep(&self, duration: Duration) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct TokioProjectionRetrySleeper;
impl ProjectionRetrySleeper for TokioProjectionRetrySleeper {
fn sleep(&self, duration: Duration) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(tokio::time::sleep(duration))
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct TokioProjectionPollSleeper;
impl ProjectionPollSleeper for TokioProjectionPollSleeper {
fn sleep(&self, duration: Duration) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(tokio::time::sleep(duration))
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ProjectionRetryPolicy {
max_retries: u32,
initial_delay: Duration,
multiplier: f64,
maximum_delay: Duration,
}
impl ProjectionRetryPolicy {
pub fn new(
max_retries: u32,
initial_delay: Duration,
multiplier: f64,
maximum_delay: Duration,
) -> Result<Self, ProjectionConfigurationError> {
if max_retries == u32::MAX {
return Err(ProjectionConfigurationError::TooManyRetries);
}
if !multiplier.is_finite() || multiplier < 1.0 {
return Err(ProjectionConfigurationError::InvalidRetryMultiplier);
}
Ok(Self {
max_retries,
initial_delay,
multiplier,
maximum_delay,
})
}
pub fn max_retries(&self) -> u32 {
self.max_retries
}
pub fn initial_delay(&self) -> Duration {
self.initial_delay
}
pub fn multiplier(&self) -> f64 {
self.multiplier
}
pub fn maximum_delay(&self) -> Duration {
self.maximum_delay
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum PostgresProjectionMode {
Batch,
Continuous(CancellationToken),
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[non_exhaustive]
pub enum ProjectionConfigurationError {
#[error("transactional projection batch size must be positive")]
ZeroBatchSize,
#[error("continuous poll interval must be positive")]
ZeroContinuousPollInterval,
#[error("retry multiplier must be finite and at least one")]
InvalidRetryMultiplier,
#[error("retry count is too large to represent the initial attempt plus retries")]
TooManyRetries,
}
#[derive(Debug, Clone)]
pub struct PostgresProjectionConfig {
selection: ProjectionSelection,
batch_size: BatchSize,
mode: PostgresProjectionMode,
retry_policy: ProjectionRetryPolicy,
retry_sleeper: Arc<dyn ProjectionRetrySleeper>,
poll_sleeper: Arc<dyn ProjectionPollSleeper>,
continuous_poll_interval: Duration,
}
impl PostgresProjectionConfig {
pub fn new(selection: ProjectionSelection) -> Self {
Self {
selection,
batch_size: BatchSize::new(100),
mode: PostgresProjectionMode::Batch,
retry_policy: ProjectionRetryPolicy {
max_retries: 0,
initial_delay: Duration::from_millis(100),
multiplier: 2.0,
maximum_delay: Duration::from_secs(30),
},
retry_sleeper: Arc::new(TokioProjectionRetrySleeper),
poll_sleeper: Arc::new(TokioProjectionPollSleeper),
continuous_poll_interval: Duration::from_secs(1),
}
}
pub fn continuous(mut self, cancellation: CancellationToken) -> Self {
self.mode = PostgresProjectionMode::Continuous(cancellation);
self
}
pub fn with_batch_size(
mut self,
batch_size: BatchSize,
) -> Result<Self, ProjectionConfigurationError> {
if usize::from(batch_size) == 0 {
return Err(ProjectionConfigurationError::ZeroBatchSize);
}
self.batch_size = batch_size;
Ok(self)
}
pub fn with_retry_policy(mut self, retry_policy: ProjectionRetryPolicy) -> Self {
self.retry_policy = retry_policy;
self
}
pub fn with_retry_sleeper(
mut self,
retry_sleeper: impl ProjectionRetrySleeper + 'static,
) -> Self {
self.retry_sleeper = Arc::new(retry_sleeper);
self
}
pub fn with_poll_sleeper(mut self, poll_sleeper: impl ProjectionPollSleeper + 'static) -> Self {
self.poll_sleeper = Arc::new(poll_sleeper);
self
}
pub fn with_continuous_poll_interval(
mut self,
continuous_poll_interval: Duration,
) -> Result<Self, ProjectionConfigurationError> {
if continuous_poll_interval.is_zero() {
return Err(ProjectionConfigurationError::ZeroContinuousPollInterval);
}
self.continuous_poll_interval = continuous_poll_interval;
Ok(self)
}
pub fn selection(&self) -> &ProjectionSelection {
&self.selection
}
pub fn batch_size(&self) -> BatchSize {
self.batch_size
}
pub fn mode(&self) -> &PostgresProjectionMode {
&self.mode
}
pub fn retry_policy(&self) -> &ProjectionRetryPolicy {
&self.retry_policy
}
pub fn retry_sleeper(&self) -> &(dyn ProjectionRetrySleeper + 'static) {
self.retry_sleeper.as_ref()
}
pub fn poll_sleeper(&self) -> &(dyn ProjectionPollSleeper + 'static) {
self.poll_sleeper.as_ref()
}
pub fn continuous_poll_interval(&self) -> Duration {
self.continuous_poll_interval
}
}