xmtp_mls/subscriptions/local_delivery/
types.rs1use thiserror::Error;
2use xmtp_common::{ErrorCode, RetryableError, time::Duration};
3use xmtp_db::StorageError;
4
5pub use xmtp_db::delivery::DeliveryFilter as LocalDeliveryFilter;
6
7use crate::subscriptions::{incoming::stream_api::RecoveryFailure, policy::StreamPolicy};
8
9const LEASE_RENEWAL_DIVISOR: u32 = 3;
10
11#[derive(Debug, Clone, Copy)]
13pub(crate) struct LocalDeliveryConfig {
14 pub batch_size: u32,
16 pub max_bytes: u64,
18 pub poll_interval: Duration,
20 pub lease_duration: Duration,
22 pub renew_interval: Duration,
24}
25
26impl Default for LocalDeliveryConfig {
27 fn default() -> Self {
28 Self::from(&StreamPolicy::default())
29 }
30}
31
32impl From<&StreamPolicy> for LocalDeliveryConfig {
33 fn from(settings: &StreamPolicy) -> Self {
34 Self {
35 batch_size: settings.max_local_read_rows,
36 max_bytes: settings.max_local_read_bytes,
37 poll_interval: settings.active_database_poll_interval,
38 lease_duration: settings.default_consumer_lease_duration,
39 renew_interval: settings.default_consumer_lease_duration / LEASE_RENEWAL_DIVISOR,
40 }
41 }
42}
43
44impl LocalDeliveryConfig {
45 pub(super) fn validate(self) -> Result<(), LocalDeliveryError> {
46 if self.batch_size == 0
47 || self.max_bytes == 0
48 || self.poll_interval.is_zero()
49 || self.renew_interval.is_zero()
50 || self.renew_interval >= self.lease_duration
51 {
52 return Err(LocalDeliveryError::InvalidConfiguration);
53 }
54 self.lease_until(0)?;
55 Ok(())
56 }
57
58 pub(super) fn lease_until(self, now: i64) -> Result<i64, LocalDeliveryError> {
59 now.checked_add(self.lease_duration_ns()?)
60 .ok_or(LocalDeliveryError::InvalidConfiguration)
61 }
62
63 pub(super) fn lease_duration_ns(self) -> Result<i64, LocalDeliveryError> {
64 i64::try_from(self.lease_duration.as_nanos())
65 .map_err(|_| LocalDeliveryError::InvalidConfiguration)
66 }
67}
68
69#[derive(Debug, Error, ErrorCode)]
70pub enum LocalDeliveryError {
71 #[error(transparent)]
73 #[error_code(inherit)]
74 SessionFailure(std::sync::Arc<LocalDeliveryError>),
75 #[error("The delivery item has no enriched message")]
77 EnrichedMessageUnavailable,
78 #[error("The message reader exhausted its network recovery budget after {attempts} attempts: {}", source.as_ref().map(|error| error.to_string()).unwrap_or_else(|| "the recovery deadline elapsed without a transport error".into()))]
80 NetworkRecoveryExhausted {
81 attempts: u32,
82 #[source]
83 source: Option<std::sync::Arc<crate::subscriptions::incoming::IncomingError>>,
84 },
85 #[error("The message reader network failed: {0}")]
88 NetworkFailure(#[source] std::sync::Arc<crate::subscriptions::incoming::IncomingError>),
89 #[error(transparent)]
91 #[error_code(inherit)]
92 Blocked(#[from] crate::server_configuration::BlockedConnection),
93 #[error(transparent)]
95 #[error_code(inherit)]
96 Storage(#[from] StorageError),
97 #[error("The delivered message was not acknowledged")]
99 AcknowledgementRejected,
100 #[error("Delivery stopped after acknowledgement persistence failed")]
102 AcknowledgementFailed,
103 #[error("The queued message belongs to an old delivery selection")]
105 SelectionChanged,
106 #[error("The local delivery configuration is invalid")]
108 InvalidConfiguration,
109 #[error("The local message reader is closed")]
111 Closed,
112}
113
114impl From<RecoveryFailure> for LocalDeliveryError {
115 fn from(failure: RecoveryFailure) -> Self {
116 match failure {
117 RecoveryFailure::Exhausted { attempts, source } => {
118 Self::NetworkRecoveryExhausted { attempts, source }
119 }
120 RecoveryFailure::Terminal(error) => Self::NetworkFailure(error),
121 }
122 }
123}
124
125impl crate::worker::NeedsDbReconnect for LocalDeliveryError {
126 #[deny(clippy::wildcard_enum_match_arm)]
127 fn needs_db_reconnect(&self) -> bool {
128 match self {
129 Self::SessionFailure(error) => error.needs_db_reconnect(),
130 Self::NetworkRecoveryExhausted { source, .. } => source
131 .as_ref()
132 .is_some_and(|error| error.needs_db_reconnect()),
133 Self::NetworkFailure(error) => error.needs_db_reconnect(),
134 Self::Storage(error) => error.db_needs_connection(),
135 Self::EnrichedMessageUnavailable
136 | Self::Blocked(..)
137 | Self::AcknowledgementRejected
138 | Self::AcknowledgementFailed
139 | Self::SelectionChanged
140 | Self::InvalidConfiguration
141 | Self::Closed => false,
142 }
143 }
144}
145
146impl RetryableError for LocalDeliveryError {
147 fn is_retryable(&self) -> bool {
148 match self {
149 Self::Storage(error) => error.is_retryable(),
150 Self::NetworkFailure(error) => xmtp_api::preflight::failure(error.as_ref())
151 .is_some_and(RetryableError::is_retryable),
152 _ => false,
153 }
154 }
155
156 fn rate_limit_delay(&self) -> Option<xmtp_common::time::Duration> {
157 match self {
158 Self::NetworkRecoveryExhausted {
159 source: Some(error),
160 ..
161 }
162 | Self::NetworkFailure(error) => error.rate_limit_delay(),
163 _ => None,
164 }
165 }
166}