mod queue;
pub use queue::*;
mod registry;
pub use registry::*;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::time::Duration;
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(default, deny_unknown_fields)]
pub struct DispatcherConfig {
pub queue_capacity: usize,
pub max_in_flight: usize,
pub delivery_timeout_ms: u64,
pub shutdown_drain_timeout_ms: u64,
pub max_attempts: usize,
pub retry_initial_backoff_ms: u64,
pub retry_max_backoff_ms: u64,
}
impl Default for DispatcherConfig {
fn default() -> Self {
Self {
queue_capacity: 512,
max_in_flight: 4,
delivery_timeout_ms: 3000,
shutdown_drain_timeout_ms: 3000,
max_attempts: 1,
retry_initial_backoff_ms: 250,
retry_max_backoff_ms: 2000,
}
}
}
impl DispatcherConfig {
pub fn validate(&self) -> Result<(), ConfigError> {
for (valid, field) in [
(
(1..=65536).contains(&self.queue_capacity),
"dispatcher.queue_capacity",
),
(
(1..=32).contains(&self.max_in_flight),
"dispatcher.max_in_flight",
),
(
(100..=30000).contains(&self.delivery_timeout_ms),
"dispatcher.delivery_timeout_ms",
),
(
self.shutdown_drain_timeout_ms <= 30000,
"dispatcher.shutdown_drain_timeout_ms",
),
(
(1..=5).contains(&self.max_attempts),
"dispatcher.max_attempts",
),
(
(50..=5000).contains(&self.retry_initial_backoff_ms),
"dispatcher.retry_initial_backoff_ms",
),
(
(self.retry_initial_backoff_ms..=10000).contains(&self.retry_max_backoff_ms),
"dispatcher.retry_max_backoff_ms",
),
] {
if !valid {
return Err(ConfigError(field));
}
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ConfigError(pub &'static str);
impl fmt::Display for ConfigError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "invalid notification configuration: {}", self.0)
}
}
impl std::error::Error for ConfigError {}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
#[repr(usize)]
pub enum EventKind {
#[default]
SlowSql,
ExecutionError,
AcquireTimeout,
}
impl EventKind {
pub const fn as_str(self) -> &'static str {
match self {
Self::SlowSql => "slow_sql",
Self::ExecutionError => "execution_error",
Self::AcquireTimeout => "acquire_timeout",
}
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Severity {
Info,
#[default]
Warning,
Error,
Critical,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct NotificationIdentity {
pub service: String,
pub instance: String,
pub environment: Option<String>,
pub cluster: Option<String>,
}
impl Severity {
pub const fn as_str(self) -> &'static str {
match self {
Self::Info => "info",
Self::Warning => "warning",
Self::Error => "error",
Self::Critical => "critical",
}
}
}
#[derive(Clone, Default)]
pub struct NotificationFields {
pub id: String,
pub event: EventKind,
pub severity: Severity,
pub occurred_at_unix_ms: u64,
pub service: String,
pub instance: String,
pub environment: Option<String>,
pub cluster: Option<String>,
pub driver: String,
pub datasource: String,
pub operation: String,
pub method: Option<String>,
pub purpose: Option<String>,
pub duration: Duration,
pub outcome: String,
pub error_kind: Option<String>,
pub prepared_sql: Option<String>,
pub trace_id: Option<String>,
pub slow: bool,
}
#[derive(Clone)]
pub struct Notification(NotificationFields);
impl Notification {
pub fn new(mut fields: NotificationFields) -> Self {
for text in [
&mut fields.id,
&mut fields.service,
&mut fields.instance,
&mut fields.driver,
&mut fields.datasource,
&mut fields.operation,
&mut fields.outcome,
] {
*text = bounded_text(text, 128);
}
for value in [
&mut fields.environment,
&mut fields.cluster,
&mut fields.purpose,
&mut fields.error_kind,
&mut fields.trace_id,
]
.into_iter()
.flatten()
{
*value = bounded_text(value, 128);
}
if let Some(value) = &mut fields.method {
*value = bounded_text(value, 512);
}
if let Some(value) = &mut fields.prepared_sql {
*value = bounded_text(value, 4096);
}
Self(fields)
}
pub fn fields(&self) -> &NotificationFields {
&self.0
}
}
pub fn bounded_text(text: &str, max_chars: usize) -> String {
if max_chars == 0 {
return String::new();
}
let mut chars = text.chars();
let mut output = String::with_capacity(text.len().min(max_chars.saturating_mul(4)));
for _ in 0..max_chars {
let Some(ch) = chars.next() else {
return output;
};
output.push(
if ch.is_control() || matches!(ch, '\u{202a}'..='\u{202e}' | '\u{2066}'..='\u{2069}') {
' '
} else {
ch
},
);
}
if chars.next().is_some() {
output.pop();
output.push('…');
}
output
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NotifyReceipt {
pub message_id: Option<String>,
pub accepted: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(usize)]
pub enum NotifyErrorKind {
Timeout,
Unavailable,
RateLimited,
Rejected,
Authentication,
InvalidRequest,
Other,
}
impl NotifyErrorKind {
pub const fn as_str(self) -> &'static str {
match self {
Self::Timeout => "timeout",
Self::Unavailable => "unavailable",
Self::RateLimited => "rate_limited",
Self::Rejected => "rejected",
Self::Authentication => "authentication",
Self::InvalidRequest => "invalid_request",
Self::Other => "other",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RetrySafety {
Never,
Safe {
retry_after: Option<Duration>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct NotifyError {
pub kind: NotifyErrorKind,
pub retry: RetrySafety,
}
impl NotifyError {
pub const fn new(kind: NotifyErrorKind) -> Self {
Self {
kind,
retry: RetrySafety::Never,
}
}
pub const fn retryable(kind: NotifyErrorKind, retry_after: Option<Duration>) -> Self {
Self {
kind,
retry: RetrySafety::Safe {
retry_after,
},
}
}
}
impl fmt::Display for NotifyError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.kind.as_str())
}
}
impl std::error::Error for NotifyError {}
#[async_trait::async_trait]
pub trait Notify: Send + Sync {
async fn notify(&self, notification: &Notification) -> Result<NotifyReceipt, NotifyError>;
}