mqtt_topic_engine/
topic_router.rs1#![allow(clippy::missing_docs_in_private_items)]
10use std::collections::{HashMap, HashSet};
11use std::fmt::Display;
12
13use arcstr::ArcStr;
14use thiserror::Error;
15
16use crate::qos::QoS;
17use crate::topic_match::TopicPath;
18use crate::topic_matcher::{TopicMatcherError, TopicMatcherNode};
19use crate::topic_pattern_item::TopicPatternError;
20use crate::topic_pattern_path::TopicPatternPath;
21
22#[derive(Error, Debug, Clone, PartialEq, Eq)]
24pub enum TopicRouterError {
25 #[error("Invalid topic pattern: {0}")]
27 InvalidPattern(#[from] TopicPatternError),
28
29 #[error("Topic matching failed: {0}")]
31 MatchingFailed(#[from] TopicMatcherError),
32
33 #[error("Subscription {id:?} not found")]
35 SubscriptionNotFound {
36 id: SubscriptionId,
38 },
39
40 #[error("Topic '{topic}' is invalid for routing: {reason}")]
42 InvalidRoutingTopic {
43 topic: String,
45 reason: String,
47 },
48
49 #[error("Internal routing state corrupted: {details}")]
51 InternalStateCorrupted {
52 details: String,
54 },
55}
56
57impl TopicRouterError {
58 pub fn subscription_not_found(id: SubscriptionId) -> Self {
60 Self::SubscriptionNotFound { id }
61 }
62
63 pub fn invalid_routing_topic(
65 topic: impl Into<String>,
66 reason: impl Into<String>,
67 ) -> Self {
68 Self::InvalidRoutingTopic {
69 topic: topic.into(),
70 reason: reason.into(),
71 }
72 }
73
74 pub fn internal_state_corrupted(details: impl Into<String>) -> Self {
76 Self::InternalStateCorrupted {
77 details: details.into(),
78 }
79 }
80}
81
82#[derive(Debug, Eq, PartialEq, Hash, Copy, Clone)]
87pub struct SubscriptionId(usize);
88
89impl Display for SubscriptionId {
90 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91 write!(f, "SubscriptionId({})", self.0)
92 }
93}
94
95type SubscriptionTable<T> = HashMap<SubscriptionId, T>;
96pub struct TopicRouter<T> {
105 topic_matcher: TopicMatcherNode<SubscriptionTable<T>>,
106 subscriptions: SubscriptionTable<(TopicPatternPath, QoS)>,
107 next_id: usize,
108}
109
110impl<T> Default for TopicRouter<T> {
111 fn default() -> Self {
112 Self::new()
113 }
114}
115
116impl<T> TopicRouter<T> {
117 pub fn new() -> Self {
119 Self {
120 topic_matcher: TopicMatcherNode::new(),
121 subscriptions: SubscriptionTable::new(),
122 next_id: 0,
123 }
124 }
125
126 pub fn add_subscription(
133 &mut self,
134 topic: TopicPatternPath,
135 qos: impl Into<QoS>,
136 subscription: T,
137 ) -> (bool, SubscriptionId) {
138 let qos = qos.into();
139 let subscription_table =
140 self.topic_matcher.get_or_create_subscription_table(&topic);
141 let needs_subscribe = subscription_table
142 .keys()
143 .map(|id| {
144 self.subscriptions
145 .get(id)
146 .unwrap_or_else(|| {
147 panic!(
148 "BUG: Subscription ID {id:?} exists in topic \
149 matcher but missing from subscriptions. Topic: \
150 {topic}"
151 )
152 })
153 .1
154 })
155 .max_by_key(|qos| *qos as u8)
156 .is_none_or(|max| qos > max);
157
158 let id = SubscriptionId(self.next_id);
159 self.next_id = self.next_id.wrapping_add(1);
160
161 subscription_table.insert(id, subscription);
162 self.subscriptions.insert(id, (topic, qos));
163
164 (needs_subscribe, id)
165 }
166
167 pub fn unsubscribe(
174 &mut self,
175 id: &SubscriptionId,
176 ) -> Result<(bool, TopicPatternPath), TopicRouterError> {
177 let topic = self.subscriptions.remove(id);
184 match topic {
185 | Some((topic_pattern, _qos)) => {
186 let resolved_segments = topic_pattern.resolve_bound_segments();
187 let topic_now_empty = self.topic_matcher.update_node(
188 &resolved_segments,
189 |table| {
190 table.remove(id);
191 },
192 )?;
193 Ok((topic_now_empty, topic_pattern))
194 }
195 | None => Err(TopicRouterError::subscription_not_found(*id)),
196 }
197 }
198
199 pub fn get_subscribers<'a>(
204 &'a self,
205 topic: &TopicPath,
206 ) -> Vec<(&'a SubscriptionId, &'a (TopicPatternPath, QoS), &'a T)> {
207 let subscribers = self.topic_matcher.find_by_path(topic);
208 subscribers
209 .into_iter()
210 .flat_map(|hash_map| hash_map.iter())
211 .map(|(id, subscription)| {
212 let topic_pattern = self
213 .subscriptions
214 .get(id)
215 .expect("Subscription ID should exist in subscriptions");
216 (id, topic_pattern, subscription)
217 })
218 .collect()
219 }
220
221 pub fn get_active_subscriptions(
225 &self,
226 ) -> impl Iterator<Item = &(TopicPatternPath, QoS)> {
227 self.subscriptions.values()
228 }
229
230 #[allow(dead_code)] fn get_max_qos_for_topic(
239 &self,
240 topic: &TopicPatternPath,
241 topic_subscriptions: &HashMap<SubscriptionId, T>,
242 ) -> QoS {
243 debug_assert!(
244 !topic_subscriptions.is_empty(),
245 "topic_subscriptions should never be empty - this is guaranteed \
246 by collect_active_subscriptions()"
247 );
248
249 let max_qos = topic_subscriptions
250 .keys()
251 .map(|id| {
252 self.subscriptions
253 .get(id)
254 .unwrap_or_else(|| {
255 panic!(
256 "BUG: Subscription ID {id:?} exists in topic \
257 matcher but missing from subscriptions. Topic: \
258 {topic}"
259 )
260 })
261 .1
262 })
263 .max_by_key(|qos| *qos as u8)
264 .unwrap();
265 max_qos
266 }
267
268 pub fn get_topics_for_unsubscribe(&self) -> HashSet<ArcStr> {
270 self.subscriptions
271 .values()
272 .map(|(topic, _)| topic.mqtt_pattern())
273 .collect()
274 }
275
276 pub fn get_topics_for_resubscribe(&self) -> HashMap<ArcStr, QoS> {
279 let mut result: HashMap<ArcStr, QoS> = HashMap::new();
280
281 for (topic, qos) in self.subscriptions.values() {
282 let mqtt_pattern = topic.mqtt_pattern();
283 result
284 .entry(mqtt_pattern)
285 .and_modify(|existing_qos| {
286 if *qos > *existing_qos {
287 *existing_qos = *qos;
288 }
289 })
290 .or_insert(*qos);
291 }
292
293 result
294 }
295
296 pub fn cleanup(&mut self) {
299 self.topic_matcher = TopicMatcherNode::new();
302 self.subscriptions.clear();
303 self.next_id = 0;
304 }
305
306 pub fn get_topic_by_id(
310 &self,
311 id: &SubscriptionId,
312 ) -> Result<&(TopicPatternPath, QoS), TopicRouterError> {
313 self.subscriptions
314 .get(id)
315 .ok_or(TopicRouterError::subscription_not_found(*id))
316 }
317}