Skip to main content

rabia_kvstore_example/
notifications.rs

1//! # Change Notification System
2//!
3//! Event-driven notification system for KVStore changes using a message bus pattern.
4
5use parking_lot::RwLock;
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::sync::Arc;
9use tokio::sync::{broadcast, mpsc};
10use tracing::debug;
11use uuid::Uuid;
12
13/// Types of changes that can occur in the store
14#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
15pub enum ChangeType {
16    Created,
17    Updated,
18    Deleted,
19    Cleared,
20}
21
22impl std::fmt::Display for ChangeType {
23    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24        match self {
25            ChangeType::Created => write!(f, "CREATED"),
26            ChangeType::Updated => write!(f, "UPDATED"),
27            ChangeType::Deleted => write!(f, "DELETED"),
28            ChangeType::Cleared => write!(f, "CLEARED"),
29        }
30    }
31}
32
33/// Notification about a change in the store
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct ChangeNotification {
36    pub key: String,
37    pub change_type: ChangeType,
38    pub old_value: Option<String>,
39    pub new_value: Option<String>,
40    pub version: u64,
41    pub timestamp: u64,
42}
43
44/// Unique identifier for a subscription
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
46pub struct SubscriptionId(Uuid);
47
48impl SubscriptionId {
49    pub fn new() -> Self {
50        Self(Uuid::new_v4())
51    }
52}
53
54impl Default for SubscriptionId {
55    fn default() -> Self {
56        Self::new()
57    }
58}
59
60/// Filter for notifications
61#[derive(Debug, Clone)]
62pub enum NotificationFilter {
63    /// All notifications
64    All,
65    /// Only notifications for specific key
66    Key(String),
67    /// Only notifications for keys with specific prefix
68    KeyPrefix(String),
69    /// Only notifications of specific type
70    ChangeType(ChangeType),
71    /// Combined filters (all must match)
72    And(Vec<NotificationFilter>),
73    /// Any of the filters can match
74    Or(Vec<NotificationFilter>),
75}
76
77impl NotificationFilter {
78    /// Check if a notification matches this filter
79    pub fn matches(&self, notification: &ChangeNotification) -> bool {
80        match self {
81            NotificationFilter::All => true,
82            NotificationFilter::Key(key) => notification.key == *key,
83            NotificationFilter::KeyPrefix(prefix) => notification.key.starts_with(prefix),
84            NotificationFilter::ChangeType(change_type) => notification.change_type == *change_type,
85            NotificationFilter::And(filters) => filters.iter().all(|f| f.matches(notification)),
86            NotificationFilter::Or(filters) => filters.iter().any(|f| f.matches(notification)),
87        }
88    }
89}
90
91/// Subscription to notifications
92pub struct Subscription {
93    pub id: SubscriptionId,
94    pub filter: NotificationFilter,
95    pub receiver: mpsc::UnboundedReceiver<ChangeNotification>,
96}
97
98/// Statistics about the notification bus
99#[derive(Debug, Clone, Default)]
100pub struct NotificationStats {
101    pub total_notifications_sent: u64,
102    pub total_subscribers: usize,
103    pub dropped_notifications: u64,
104}
105
106/// Message bus for distributing change notifications
107pub struct NotificationBus {
108    /// Broadcast channel for all notifications
109    broadcast_tx: broadcast::Sender<ChangeNotification>,
110
111    /// Individual subscriber channels
112    #[allow(clippy::type_complexity)]
113    subscribers: Arc<
114        RwLock<
115            HashMap<
116                SubscriptionId,
117                (
118                    NotificationFilter,
119                    mpsc::UnboundedSender<ChangeNotification>,
120                ),
121            >,
122        >,
123    >,
124
125    /// Statistics
126    stats: Arc<RwLock<NotificationStats>>,
127}
128
129impl NotificationBus {
130    /// Create a new notification bus
131    pub fn new() -> Self {
132        let (broadcast_tx, _) = broadcast::channel(1000);
133
134        Self {
135            broadcast_tx,
136            subscribers: Arc::new(RwLock::new(HashMap::new())),
137            stats: Arc::new(RwLock::new(NotificationStats::default())),
138        }
139    }
140
141    /// Subscribe to notifications with a filter
142    pub fn subscribe(&self, filter: NotificationFilter) -> Subscription {
143        let id = SubscriptionId::new();
144        let (tx, rx) = mpsc::unbounded_channel();
145
146        {
147            let mut subscribers = self.subscribers.write();
148            subscribers.insert(id, (filter.clone(), tx));
149        }
150
151        {
152            let mut stats = self.stats.write();
153            stats.total_subscribers += 1;
154        }
155
156        debug!(
157            "New subscription created: {:?} with filter: {:?}",
158            id, filter
159        );
160
161        Subscription {
162            id,
163            filter,
164            receiver: rx,
165        }
166    }
167
168    /// Subscribe to all notifications (convenience method)
169    pub fn subscribe_all(&self) -> Subscription {
170        self.subscribe(NotificationFilter::All)
171    }
172
173    /// Subscribe to notifications for a specific key
174    pub fn subscribe_key(&self, key: &str) -> Subscription {
175        self.subscribe(NotificationFilter::Key(key.to_string()))
176    }
177
178    /// Subscribe to notifications for keys with a specific prefix
179    pub fn subscribe_prefix(&self, prefix: &str) -> Subscription {
180        self.subscribe(NotificationFilter::KeyPrefix(prefix.to_string()))
181    }
182
183    /// Subscribe to notifications of a specific change type
184    pub fn subscribe_change_type(&self, change_type: ChangeType) -> Subscription {
185        self.subscribe(NotificationFilter::ChangeType(change_type))
186    }
187
188    /// Unsubscribe from notifications
189    pub fn unsubscribe(&self, subscription_id: SubscriptionId) {
190        let mut subscribers = self.subscribers.write();
191        if subscribers.remove(&subscription_id).is_some() {
192            let mut stats = self.stats.write();
193            stats.total_subscribers = stats.total_subscribers.saturating_sub(1);
194            debug!("Subscription removed: {:?}", subscription_id);
195        }
196    }
197
198    /// Publish a notification to all subscribers
199    pub async fn publish(&self, notification: ChangeNotification) {
200        // Update statistics
201        {
202            let mut stats = self.stats.write();
203            stats.total_notifications_sent += 1;
204        }
205
206        // Send to broadcast channel (for global listeners)
207        if self.broadcast_tx.send(notification.clone()).is_err() {
208            // No broadcast receivers, that's fine
209        }
210
211        // Send to individual subscribers with filtering
212        let subscribers = self.subscribers.read();
213        let mut dropped_count = 0;
214
215        for (filter, sender) in subscribers.values() {
216            if filter.matches(&notification) && sender.send(notification.clone()).is_err() {
217                // Subscriber channel is closed, will be cleaned up later
218                dropped_count += 1;
219            }
220        }
221
222        if dropped_count > 0 {
223            let mut stats = self.stats.write();
224            stats.dropped_notifications += dropped_count;
225            debug!(
226                "Dropped {} notifications due to closed channels",
227                dropped_count
228            );
229        }
230
231        debug!(
232            "Published notification: key={}, type={:?}",
233            notification.key, notification.change_type
234        );
235    }
236
237    /// Get a broadcast receiver for all notifications
238    pub fn broadcast_receiver(&self) -> broadcast::Receiver<ChangeNotification> {
239        self.broadcast_tx.subscribe()
240    }
241
242    /// Get current statistics
243    pub fn get_stats(&self) -> NotificationStats {
244        let stats = self.stats.read();
245        let subscribers = self.subscribers.read();
246
247        NotificationStats {
248            total_notifications_sent: stats.total_notifications_sent,
249            total_subscribers: subscribers.len(),
250            dropped_notifications: stats.dropped_notifications,
251        }
252    }
253
254    /// Clean up closed subscriber channels
255    pub fn cleanup_closed_subscribers(&self) {
256        let mut subscribers = self.subscribers.write();
257        let initial_count = subscribers.len();
258
259        subscribers.retain(|_, (_, sender)| !sender.is_closed());
260
261        let removed = initial_count - subscribers.len();
262        if removed > 0 {
263            debug!("Cleaned up {} closed subscriber channels", removed);
264        }
265    }
266
267    /// Get the number of active subscribers
268    pub fn subscriber_count(&self) -> usize {
269        self.subscribers.read().len()
270    }
271}
272
273impl Default for NotificationBus {
274    fn default() -> Self {
275        Self::new()
276    }
277}
278
279/// Notification listener that can be used for async processing
280pub struct NotificationListener {
281    subscription: Subscription,
282    name: String,
283}
284
285impl NotificationListener {
286    /// Create a new notification listener
287    pub fn new(subscription: Subscription, name: String) -> Self {
288        Self { subscription, name }
289    }
290
291    /// Start listening for notifications and process them with the given handler
292    pub async fn listen<F, Fut>(&mut self, mut handler: F)
293    where
294        F: FnMut(ChangeNotification) -> Fut,
295        Fut: std::future::Future<Output = ()>,
296    {
297        debug!("Starting notification listener: {}", self.name);
298
299        while let Some(notification) = self.subscription.receiver.recv().await {
300            debug!(
301                "Listener {} received notification: {:?}",
302                self.name, notification
303            );
304            handler(notification).await;
305        }
306
307        debug!("Notification listener {} stopped", self.name);
308    }
309
310    /// Get the subscription ID
311    pub fn subscription_id(&self) -> SubscriptionId {
312        self.subscription.id
313    }
314}
315
316#[cfg(test)]
317mod tests {
318    use super::*;
319
320    #[tokio::test]
321    async fn test_notification_bus_basic() {
322        let bus = NotificationBus::new();
323        let mut subscription = bus.subscribe_all();
324
325        let notification = ChangeNotification {
326            key: "test_key".to_string(),
327            change_type: ChangeType::Created,
328            old_value: None,
329            new_value: Some("test_value".to_string()),
330            version: 1,
331            timestamp: 123456789,
332        };
333
334        // Publish notification
335        bus.publish(notification.clone()).await;
336
337        // Receive notification
338        let received = subscription.receiver.recv().await.unwrap();
339        assert_eq!(received.key, notification.key);
340        assert_eq!(received.change_type, notification.change_type);
341        assert_eq!(received.new_value, notification.new_value);
342    }
343
344    #[tokio::test]
345    async fn test_notification_filtering() {
346        let bus = NotificationBus::new();
347        let mut key_subscription = bus.subscribe_key("specific_key");
348        let mut prefix_subscription = bus.subscribe_prefix("prefix_");
349        let mut type_subscription = bus.subscribe_change_type(ChangeType::Updated);
350
351        // Notification that should match key filter
352        let key_notification = ChangeNotification {
353            key: "specific_key".to_string(),
354            change_type: ChangeType::Created,
355            old_value: None,
356            new_value: Some("value".to_string()),
357            version: 1,
358            timestamp: 123456789,
359        };
360
361        // Notification that should match prefix filter
362        let prefix_notification = ChangeNotification {
363            key: "prefix_test".to_string(),
364            change_type: ChangeType::Created,
365            old_value: None,
366            new_value: Some("value".to_string()),
367            version: 2,
368            timestamp: 123456790,
369        };
370
371        // Notification that should match type filter
372        let type_notification = ChangeNotification {
373            key: "any_key".to_string(),
374            change_type: ChangeType::Updated,
375            old_value: Some("old".to_string()),
376            new_value: Some("new".to_string()),
377            version: 3,
378            timestamp: 123456791,
379        };
380
381        // Publish all notifications
382        bus.publish(key_notification.clone()).await;
383        bus.publish(prefix_notification.clone()).await;
384        bus.publish(type_notification.clone()).await;
385
386        // Check key subscription received only the key notification
387        let received = key_subscription.receiver.recv().await.unwrap();
388        assert_eq!(received.key, "specific_key");
389
390        // Check prefix subscription received only the prefix notification
391        let received = prefix_subscription.receiver.recv().await.unwrap();
392        assert_eq!(received.key, "prefix_test");
393
394        // Check type subscription received only the type notification
395        let received = type_subscription.receiver.recv().await.unwrap();
396        assert_eq!(received.change_type, ChangeType::Updated);
397    }
398
399    #[tokio::test]
400    async fn test_notification_stats() {
401        let bus = NotificationBus::new();
402        let _subscription = bus.subscribe_all();
403
404        let notification = ChangeNotification {
405            key: "test".to_string(),
406            change_type: ChangeType::Created,
407            old_value: None,
408            new_value: Some("value".to_string()),
409            version: 1,
410            timestamp: 123456789,
411        };
412
413        bus.publish(notification).await;
414
415        let stats = bus.get_stats();
416        assert_eq!(stats.total_notifications_sent, 1);
417        assert_eq!(stats.total_subscribers, 1);
418    }
419
420    #[test]
421    fn test_notification_filter_logic() {
422        let notification = ChangeNotification {
423            key: "test_key".to_string(),
424            change_type: ChangeType::Updated,
425            old_value: Some("old".to_string()),
426            new_value: Some("new".to_string()),
427            version: 1,
428            timestamp: 123456789,
429        };
430
431        // Test individual filters
432        assert!(NotificationFilter::All.matches(&notification));
433        assert!(NotificationFilter::Key("test_key".to_string()).matches(&notification));
434        assert!(!NotificationFilter::Key("other_key".to_string()).matches(&notification));
435        assert!(NotificationFilter::KeyPrefix("test_".to_string()).matches(&notification));
436        assert!(!NotificationFilter::KeyPrefix("other_".to_string()).matches(&notification));
437        assert!(NotificationFilter::ChangeType(ChangeType::Updated).matches(&notification));
438        assert!(!NotificationFilter::ChangeType(ChangeType::Created).matches(&notification));
439
440        // Test AND filter
441        let and_filter = NotificationFilter::And(vec![
442            NotificationFilter::KeyPrefix("test_".to_string()),
443            NotificationFilter::ChangeType(ChangeType::Updated),
444        ]);
445        assert!(and_filter.matches(&notification));
446
447        // Test OR filter
448        let or_filter = NotificationFilter::Or(vec![
449            NotificationFilter::Key("wrong_key".to_string()),
450            NotificationFilter::ChangeType(ChangeType::Updated),
451        ]);
452        assert!(or_filter.matches(&notification));
453    }
454}