1mod queue;
11pub use queue::*;
12mod registry;
13pub use registry::*;
14
15use serde::{Deserialize, Serialize};
16use std::fmt;
17use std::time::Duration;
18
19#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
21#[serde(default, deny_unknown_fields)]
22pub struct DispatcherConfig {
23 pub queue_capacity: usize,
25 pub max_in_flight: usize,
27 pub delivery_timeout_ms: u64,
29 pub shutdown_drain_timeout_ms: u64,
31 pub max_attempts: usize,
33 pub retry_initial_backoff_ms: u64,
35 pub retry_max_backoff_ms: u64,
37}
38
39impl Default for DispatcherConfig {
40 fn default() -> Self {
46 Self {
47 queue_capacity: 512,
48 max_in_flight: 4,
49 delivery_timeout_ms: 3000,
50 shutdown_drain_timeout_ms: 3000,
51 max_attempts: 1,
52 retry_initial_backoff_ms: 250,
53 retry_max_backoff_ms: 2000,
54 }
55 }
56}
57
58impl DispatcherConfig {
59 pub fn validate(&self) -> Result<(), ConfigError> {
65 for (valid, field) in [
66 (
67 (1..=65536).contains(&self.queue_capacity),
68 "dispatcher.queue_capacity",
69 ),
70 (
71 (1..=32).contains(&self.max_in_flight),
72 "dispatcher.max_in_flight",
73 ),
74 (
75 (100..=30000).contains(&self.delivery_timeout_ms),
76 "dispatcher.delivery_timeout_ms",
77 ),
78 (
79 self.shutdown_drain_timeout_ms <= 30000,
80 "dispatcher.shutdown_drain_timeout_ms",
81 ),
82 (
83 (1..=5).contains(&self.max_attempts),
84 "dispatcher.max_attempts",
85 ),
86 (
87 (50..=5000).contains(&self.retry_initial_backoff_ms),
88 "dispatcher.retry_initial_backoff_ms",
89 ),
90 (
91 (self.retry_initial_backoff_ms..=10000).contains(&self.retry_max_backoff_ms),
92 "dispatcher.retry_max_backoff_ms",
93 ),
94 ] {
95 if !valid {
96 return Err(ConfigError(field));
97 }
98 }
99 Ok(())
100 }
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub struct ConfigError(pub &'static str);
106
107impl fmt::Display for ConfigError {
108 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
115 write!(formatter, "invalid notification configuration: {}", self.0)
116 }
117}
118impl std::error::Error for ConfigError {}
119
120#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
122#[serde(rename_all = "snake_case")]
123#[repr(usize)]
124pub enum EventKind {
125 #[default]
126 SlowSql,
128 ExecutionError,
130 AcquireTimeout,
132}
133
134impl EventKind {
135 pub const fn as_str(self) -> &'static str {
141 match self {
142 Self::SlowSql => "slow_sql",
143 Self::ExecutionError => "execution_error",
144 Self::AcquireTimeout => "acquire_timeout",
145 }
146 }
147}
148
149#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
151#[serde(rename_all = "snake_case")]
152pub enum Severity {
153 Info,
155 #[default]
156 Warning,
158 Error,
160 Critical,
162}
163
164#[derive(Debug, Clone, Default, PartialEq, Eq)]
166pub struct NotificationIdentity {
167 pub service: String,
169 pub instance: String,
171 pub environment: Option<String>,
173 pub cluster: Option<String>,
175}
176
177impl Severity {
178 pub const fn as_str(self) -> &'static str {
184 match self {
185 Self::Info => "info",
186 Self::Warning => "warning",
187 Self::Error => "error",
188 Self::Critical => "critical",
189 }
190 }
191}
192
193#[derive(Clone, Default)]
195pub struct NotificationFields {
196 pub id: String,
198 pub event: EventKind,
200 pub severity: Severity,
202 pub occurred_at_unix_ms: u64,
204 pub service: String,
206 pub instance: String,
208 pub environment: Option<String>,
210 pub cluster: Option<String>,
212 pub driver: String,
214 pub datasource: String,
216 pub operation: String,
218 pub method: Option<String>,
220 pub purpose: Option<String>,
222 pub duration: Duration,
224 pub outcome: String,
226 pub error_kind: Option<String>,
228 pub prepared_sql: Option<String>,
230 pub trace_id: Option<String>,
232 pub slow: bool,
234}
235
236#[derive(Clone)]
238pub struct Notification(NotificationFields);
239
240impl Notification {
241 pub fn new(mut fields: NotificationFields) -> Self {
248 for text in [
249 &mut fields.id,
250 &mut fields.service,
251 &mut fields.instance,
252 &mut fields.driver,
253 &mut fields.datasource,
254 &mut fields.operation,
255 &mut fields.outcome,
256 ] {
257 *text = bounded_text(text, 128);
258 }
259 for value in [
260 &mut fields.environment,
261 &mut fields.cluster,
262 &mut fields.purpose,
263 &mut fields.error_kind,
264 &mut fields.trace_id,
265 ]
266 .into_iter()
267 .flatten()
268 {
269 *value = bounded_text(value, 128);
270 }
271 if let Some(value) = &mut fields.method {
272 *value = bounded_text(value, 512);
273 }
274 if let Some(value) = &mut fields.prepared_sql {
275 *value = bounded_text(value, 4096);
276 }
277 Self(fields)
278 }
279
280 pub fn fields(&self) -> &NotificationFields {
286 &self.0
287 }
288}
289
290pub fn bounded_text(text: &str, max_chars: usize) -> String {
298 if max_chars == 0 {
299 return String::new();
300 }
301 let mut chars = text.chars();
302 let mut output = String::with_capacity(text.len().min(max_chars.saturating_mul(4)));
303 for _ in 0..max_chars {
304 let Some(ch) = chars.next() else {
305 return output;
306 };
307 output.push(
308 if ch.is_control() || matches!(ch, '\u{202a}'..='\u{202e}' | '\u{2066}'..='\u{2069}') {
309 ' '
310 } else {
311 ch
312 },
313 );
314 }
315 if chars.next().is_some() {
316 output.pop();
317 output.push('…');
318 }
319 output
320}
321
322#[derive(Debug, Clone, PartialEq, Eq)]
324pub struct NotifyReceipt {
325 pub message_id: Option<String>,
327 pub accepted: bool,
329}
330
331#[derive(Debug, Clone, Copy, PartialEq, Eq)]
333#[repr(usize)]
334pub enum NotifyErrorKind {
335 Timeout,
337 Unavailable,
339 RateLimited,
341 Rejected,
343 Authentication,
345 InvalidRequest,
347 Other,
349}
350
351impl NotifyErrorKind {
352 pub const fn as_str(self) -> &'static str {
358 match self {
359 Self::Timeout => "timeout",
360 Self::Unavailable => "unavailable",
361 Self::RateLimited => "rate_limited",
362 Self::Rejected => "rejected",
363 Self::Authentication => "authentication",
364 Self::InvalidRequest => "invalid_request",
365 Self::Other => "other",
366 }
367 }
368}
369
370#[derive(Debug, Clone, Copy, PartialEq, Eq)]
372pub enum RetrySafety {
373 Never,
375 Safe {
377 retry_after: Option<Duration>,
379 },
380}
381
382#[derive(Debug, Clone, Copy, PartialEq, Eq)]
384pub struct NotifyError {
385 pub kind: NotifyErrorKind,
387 pub retry: RetrySafety,
389}
390
391impl NotifyError {
392 pub const fn new(kind: NotifyErrorKind) -> Self {
399 Self {
400 kind,
401 retry: RetrySafety::Never,
402 }
403 }
404 pub const fn retryable(kind: NotifyErrorKind, retry_after: Option<Duration>) -> Self {
412 Self {
413 kind,
414 retry: RetrySafety::Safe {
415 retry_after,
416 },
417 }
418 }
419}
420
421impl fmt::Display for NotifyError {
422 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
429 formatter.write_str(self.kind.as_str())
430 }
431}
432impl std::error::Error for NotifyError {}
433
434#[async_trait::async_trait]
439pub trait Notify: Send + Sync {
440 async fn notify(&self, notification: &Notification) -> Result<NotifyReceipt, NotifyError>;
447}