Skip to main content

oximedia_distributed/
message_bus.rs

1//! Publish/subscribe message bus for distributed coordination.
2//!
3//! This module implements an in-process message bus that supports
4//! typed topics, publish/subscribe patterns, and message filtering
5//! for coordinating distributed encoding nodes.
6
7#![allow(dead_code)]
8
9use std::collections::HashMap;
10use uuid::Uuid;
11
12/// Types of messages that can be sent on the bus.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
14pub enum MessageType {
15    /// A task has been submitted
16    TaskSubmitted,
17    /// A task has been assigned to a worker
18    TaskAssigned,
19    /// A task has completed
20    TaskCompleted,
21    /// A task has failed
22    TaskFailed,
23    /// A node has joined the cluster
24    NodeJoined,
25    /// A node has left the cluster
26    NodeLeft,
27    /// A node's health status changed
28    HealthChanged,
29    /// A heartbeat from a node
30    Heartbeat,
31    /// Cluster configuration changed
32    ConfigChanged,
33    /// Custom application-level message
34    Custom,
35}
36
37impl MessageType {
38    /// Returns the topic string for this message type.
39    #[must_use]
40    pub fn topic(&self) -> &'static str {
41        match self {
42            Self::TaskSubmitted => "task.submitted",
43            Self::TaskAssigned => "task.assigned",
44            Self::TaskCompleted => "task.completed",
45            Self::TaskFailed => "task.failed",
46            Self::NodeJoined => "node.joined",
47            Self::NodeLeft => "node.left",
48            Self::HealthChanged => "node.health",
49            Self::Heartbeat => "node.heartbeat",
50            Self::ConfigChanged => "cluster.config",
51            Self::Custom => "custom",
52        }
53    }
54
55    /// Returns true if this is a task-related message.
56    #[must_use]
57    pub fn is_task_event(&self) -> bool {
58        matches!(
59            self,
60            Self::TaskSubmitted | Self::TaskAssigned | Self::TaskCompleted | Self::TaskFailed
61        )
62    }
63
64    /// Returns true if this is a node-related message.
65    #[must_use]
66    pub fn is_node_event(&self) -> bool {
67        matches!(
68            self,
69            Self::NodeJoined | Self::NodeLeft | Self::HealthChanged | Self::Heartbeat
70        )
71    }
72}
73
74/// A message that flows through the bus.
75#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
76pub struct BusMessage {
77    /// Unique message identifier
78    pub id: Uuid,
79    /// Message type / topic
80    pub message_type: MessageType,
81    /// Source node that sent the message
82    pub source: Uuid,
83    /// Serialized payload
84    pub payload: String,
85    /// Unix timestamp when the message was created
86    pub timestamp: i64,
87    /// Optional correlation ID for request-reply patterns
88    pub correlation_id: Option<Uuid>,
89    /// Message headers (metadata)
90    pub headers: HashMap<String, String>,
91}
92
93impl BusMessage {
94    /// Creates a new bus message.
95    #[must_use]
96    pub fn new(message_type: MessageType, source: Uuid, payload: &str) -> Self {
97        Self {
98            id: Uuid::new_v4(),
99            message_type,
100            source,
101            payload: payload.to_string(),
102            timestamp: chrono::Utc::now().timestamp(),
103            correlation_id: None,
104            headers: HashMap::new(),
105        }
106    }
107
108    /// Sets a correlation ID for request-reply patterns.
109    #[must_use]
110    pub fn with_correlation(mut self, correlation_id: Uuid) -> Self {
111        self.correlation_id = Some(correlation_id);
112        self
113    }
114
115    /// Adds a header to the message.
116    #[must_use]
117    pub fn with_header(mut self, key: &str, value: &str) -> Self {
118        self.headers.insert(key.to_string(), value.to_string());
119        self
120    }
121
122    /// Returns the age of the message in seconds relative to a reference time.
123    #[must_use]
124    pub fn age_secs(&self, now: i64) -> i64 {
125        now - self.timestamp
126    }
127}
128
129/// Subscription identifier.
130#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
131pub struct SubscriptionId(Uuid);
132
133impl SubscriptionId {
134    /// Creates a new subscription ID.
135    #[must_use]
136    fn new() -> Self {
137        Self(Uuid::new_v4())
138    }
139}
140
141/// A subscription to specific message types.
142#[derive(Debug, Clone)]
143struct Subscription {
144    /// Subscription identifier
145    id: SubscriptionId,
146    /// Subscriber identifier
147    subscriber_id: Uuid,
148    /// Message types this subscription listens for
149    types: Vec<MessageType>,
150}
151
152/// Bus statistics for monitoring.
153#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
154pub struct BusStats {
155    /// Total messages published
156    pub messages_published: u64,
157    /// Total messages delivered to subscribers
158    pub messages_delivered: u64,
159    /// Total messages dropped (no subscribers)
160    pub messages_dropped: u64,
161    /// Current number of active subscriptions
162    pub active_subscriptions: u64,
163}
164
165/// An in-process publish/subscribe message bus.
166///
167/// Supports topic-based subscriptions where subscribers can listen
168/// for specific message types. Messages published to the bus are
169/// stored in per-subscriber mailboxes for retrieval.
170#[derive(Debug)]
171pub struct MessageBus {
172    /// Active subscriptions
173    subscriptions: Vec<Subscription>,
174    /// Per-subscriber mailbox
175    mailboxes: HashMap<SubscriptionId, Vec<BusMessage>>,
176    /// Maximum mailbox size
177    max_mailbox_size: usize,
178    /// Statistics
179    stats: BusStats,
180}
181
182impl MessageBus {
183    /// Creates a new message bus.
184    #[must_use]
185    pub fn new() -> Self {
186        Self {
187            subscriptions: Vec::new(),
188            mailboxes: HashMap::new(),
189            max_mailbox_size: 1000,
190            stats: BusStats::default(),
191        }
192    }
193
194    /// Creates a new message bus with a custom mailbox size limit.
195    #[must_use]
196    pub fn with_mailbox_size(max_size: usize) -> Self {
197        Self {
198            subscriptions: Vec::new(),
199            mailboxes: HashMap::new(),
200            max_mailbox_size: max_size,
201            stats: BusStats::default(),
202        }
203    }
204
205    /// Subscribes to specific message types.
206    ///
207    /// Returns a `SubscriptionId` that can be used to receive messages
208    /// or unsubscribe later.
209    pub fn subscribe(&mut self, subscriber_id: Uuid, types: Vec<MessageType>) -> SubscriptionId {
210        let sub_id = SubscriptionId::new();
211        self.subscriptions.push(Subscription {
212            id: sub_id,
213            subscriber_id,
214            types,
215        });
216        self.mailboxes.insert(sub_id, Vec::new());
217        self.stats.active_subscriptions += 1;
218        sub_id
219    }
220
221    /// Unsubscribes and removes the subscription.
222    pub fn unsubscribe(&mut self, sub_id: &SubscriptionId) -> bool {
223        let before = self.subscriptions.len();
224        self.subscriptions.retain(|s| s.id != *sub_id);
225        self.mailboxes.remove(sub_id);
226        let removed = self.subscriptions.len() < before;
227        if removed {
228            self.stats.active_subscriptions = self.stats.active_subscriptions.saturating_sub(1);
229        }
230        removed
231    }
232
233    /// Publishes a message to all matching subscribers.
234    ///
235    /// Returns the number of subscribers the message was delivered to.
236    pub fn publish(&mut self, message: BusMessage) -> usize {
237        self.stats.messages_published += 1;
238
239        let matching_subs: Vec<SubscriptionId> = self
240            .subscriptions
241            .iter()
242            .filter(|sub| sub.types.contains(&message.message_type))
243            .map(|sub| sub.id)
244            .collect();
245
246        if matching_subs.is_empty() {
247            self.stats.messages_dropped += 1;
248            return 0;
249        }
250
251        let mut delivered = 0;
252        for sub_id in &matching_subs {
253            if let Some(mailbox) = self.mailboxes.get_mut(sub_id) {
254                if mailbox.len() < self.max_mailbox_size {
255                    mailbox.push(message.clone());
256                    delivered += 1;
257                }
258            }
259        }
260
261        self.stats.messages_delivered += delivered as u64;
262        delivered
263    }
264
265    /// Receives all pending messages for a subscription.
266    ///
267    /// Drains the mailbox, returning all messages.
268    pub fn receive(&mut self, sub_id: &SubscriptionId) -> Vec<BusMessage> {
269        self.mailboxes
270            .get_mut(sub_id)
271            .map(std::mem::take)
272            .unwrap_or_default()
273    }
274
275    /// Returns the number of pending messages for a subscription.
276    #[must_use]
277    pub fn pending_count(&self, sub_id: &SubscriptionId) -> usize {
278        self.mailboxes.get(sub_id).map_or(0, Vec::len)
279    }
280
281    /// Returns the total number of active subscriptions.
282    #[must_use]
283    pub fn subscription_count(&self) -> usize {
284        self.subscriptions.len()
285    }
286
287    /// Returns bus statistics.
288    #[must_use]
289    pub fn stats(&self) -> &BusStats {
290        &self.stats
291    }
292
293    /// Clears all mailboxes without removing subscriptions.
294    pub fn clear_mailboxes(&mut self) {
295        for mailbox in self.mailboxes.values_mut() {
296            mailbox.clear();
297        }
298    }
299}
300
301impl Default for MessageBus {
302    fn default() -> Self {
303        Self::new()
304    }
305}
306
307#[cfg(test)]
308mod tests {
309    use super::*;
310
311    fn src() -> Uuid {
312        Uuid::new_v4()
313    }
314
315    #[test]
316    fn test_message_type_topics() {
317        assert_eq!(MessageType::TaskSubmitted.topic(), "task.submitted");
318        assert_eq!(MessageType::NodeJoined.topic(), "node.joined");
319        assert_eq!(MessageType::Custom.topic(), "custom");
320    }
321
322    #[test]
323    fn test_message_type_is_task_event() {
324        assert!(MessageType::TaskSubmitted.is_task_event());
325        assert!(MessageType::TaskCompleted.is_task_event());
326        assert!(!MessageType::NodeJoined.is_task_event());
327        assert!(!MessageType::Heartbeat.is_task_event());
328    }
329
330    #[test]
331    fn test_message_type_is_node_event() {
332        assert!(MessageType::NodeJoined.is_node_event());
333        assert!(MessageType::Heartbeat.is_node_event());
334        assert!(!MessageType::TaskSubmitted.is_node_event());
335    }
336
337    #[test]
338    fn test_bus_message_creation() {
339        let s = src();
340        let msg = BusMessage::new(MessageType::TaskSubmitted, s, "{\"task_id\":\"abc\"}");
341        assert_eq!(msg.message_type, MessageType::TaskSubmitted);
342        assert_eq!(msg.source, s);
343        assert!(msg.correlation_id.is_none());
344    }
345
346    #[test]
347    fn test_bus_message_with_correlation() {
348        let corr = Uuid::new_v4();
349        let msg = BusMessage::new(MessageType::TaskCompleted, src(), "{}").with_correlation(corr);
350        assert_eq!(msg.correlation_id, Some(corr));
351    }
352
353    #[test]
354    fn test_bus_message_with_header() {
355        let msg = BusMessage::new(MessageType::Custom, src(), "{}")
356            .with_header("priority", "high")
357            .with_header("region", "us-east");
358        assert_eq!(msg.headers.len(), 2);
359        assert_eq!(
360            msg.headers
361                .get("priority")
362                .expect("get should return a value"),
363            "high"
364        );
365    }
366
367    #[test]
368    fn test_bus_message_age() {
369        let mut msg = BusMessage::new(MessageType::Heartbeat, src(), "{}");
370        msg.timestamp = 1000;
371        assert_eq!(msg.age_secs(1050), 50);
372    }
373
374    #[test]
375    fn test_subscribe_and_publish() {
376        let mut bus = MessageBus::new();
377        let sub = bus.subscribe(src(), vec![MessageType::TaskSubmitted]);
378        let msg = BusMessage::new(MessageType::TaskSubmitted, src(), "{}");
379        let delivered = bus.publish(msg);
380        assert_eq!(delivered, 1);
381        let received = bus.receive(&sub);
382        assert_eq!(received.len(), 1);
383    }
384
385    #[test]
386    fn test_publish_no_subscribers() {
387        let mut bus = MessageBus::new();
388        let msg = BusMessage::new(MessageType::TaskSubmitted, src(), "{}");
389        let delivered = bus.publish(msg);
390        assert_eq!(delivered, 0);
391        assert_eq!(bus.stats().messages_dropped, 1);
392    }
393
394    #[test]
395    fn test_subscribe_filters_by_type() {
396        let mut bus = MessageBus::new();
397        let sub = bus.subscribe(src(), vec![MessageType::TaskCompleted]);
398        // Publish a different type
399        bus.publish(BusMessage::new(MessageType::TaskSubmitted, src(), "{}"));
400        let received = bus.receive(&sub);
401        assert!(received.is_empty());
402    }
403
404    #[test]
405    fn test_multiple_subscribers() {
406        let mut bus = MessageBus::new();
407        let sub1 = bus.subscribe(src(), vec![MessageType::NodeJoined]);
408        let sub2 = bus.subscribe(src(), vec![MessageType::NodeJoined]);
409        bus.publish(BusMessage::new(MessageType::NodeJoined, src(), "{}"));
410        assert_eq!(bus.receive(&sub1).len(), 1);
411        assert_eq!(bus.receive(&sub2).len(), 1);
412    }
413
414    #[test]
415    fn test_unsubscribe() {
416        let mut bus = MessageBus::new();
417        let sub = bus.subscribe(src(), vec![MessageType::Heartbeat]);
418        assert_eq!(bus.subscription_count(), 1);
419        assert!(bus.unsubscribe(&sub));
420        assert_eq!(bus.subscription_count(), 0);
421    }
422
423    #[test]
424    fn test_pending_count() {
425        let mut bus = MessageBus::new();
426        let sub = bus.subscribe(src(), vec![MessageType::TaskFailed]);
427        assert_eq!(bus.pending_count(&sub), 0);
428        bus.publish(BusMessage::new(MessageType::TaskFailed, src(), "{}"));
429        bus.publish(BusMessage::new(MessageType::TaskFailed, src(), "{}"));
430        assert_eq!(bus.pending_count(&sub), 2);
431    }
432
433    #[test]
434    fn test_mailbox_size_limit() {
435        let mut bus = MessageBus::with_mailbox_size(2);
436        let sub = bus.subscribe(src(), vec![MessageType::Heartbeat]);
437        bus.publish(BusMessage::new(MessageType::Heartbeat, src(), "1"));
438        bus.publish(BusMessage::new(MessageType::Heartbeat, src(), "2"));
439        bus.publish(BusMessage::new(MessageType::Heartbeat, src(), "3"));
440        assert_eq!(bus.pending_count(&sub), 2); // capped at 2
441    }
442
443    #[test]
444    fn test_clear_mailboxes() {
445        let mut bus = MessageBus::new();
446        let sub = bus.subscribe(src(), vec![MessageType::TaskAssigned]);
447        bus.publish(BusMessage::new(MessageType::TaskAssigned, src(), "{}"));
448        bus.clear_mailboxes();
449        assert_eq!(bus.pending_count(&sub), 0);
450        assert_eq!(bus.subscription_count(), 1); // sub still exists
451    }
452
453    #[test]
454    fn test_bus_stats() {
455        let mut bus = MessageBus::new();
456        let _sub = bus.subscribe(src(), vec![MessageType::TaskSubmitted]);
457        bus.publish(BusMessage::new(MessageType::TaskSubmitted, src(), "{}"));
458        bus.publish(BusMessage::new(MessageType::NodeLeft, src(), "{}")); // no subscriber
459        assert_eq!(bus.stats().messages_published, 2);
460        assert_eq!(bus.stats().messages_delivered, 1);
461        assert_eq!(bus.stats().messages_dropped, 1);
462    }
463}