1use alloc::collections::BTreeSet;
4use alloc::vec::Vec;
5use core::fmt;
6
7use crate::broker::{BrokerEndpoint, Choice, Wanted, choose, log_choice, rank_brokers};
8use crate::instance::{InstanceKey, InstanceTable, Sighting};
9use crate::{BROKER_TYPES, BrokerMode, BrokerType, Instant, TCP_BROKER_TYPES};
10
11const REQUERY_FAST_MS: u64 = 3_000;
13const REQUERY_SLOW_MS: u64 = 30_000;
15const REQUERY_FAST_ATTEMPTS: u32 = 3;
17
18#[derive(Clone, Debug, PartialEq, Eq)]
21#[non_exhaustive]
22pub struct BrokerConfig {
23 pub mode: Option<BrokerMode>,
25 pub url: Option<BrokerEndpoint>,
27 pub wanted: Option<Wanted>,
29 pub tls: bool,
31 pub accept: Option<Vec<BrokerType>>,
37 pub allow_unmatched: bool,
40 pub settle_ms: u64,
42 pub fallback_after_ms: u64,
45}
46
47impl Default for BrokerConfig {
48 fn default() -> Self {
49 Self {
50 mode: None,
51 url: None,
52 wanted: None,
53 tls: false,
54 accept: None,
55 allow_unmatched: false,
56 settle_ms: 3_000,
57 fallback_after_ms: 15_000,
58 }
59 }
60}
61
62impl BrokerConfig {
63 pub fn effective_mode(&self) -> BrokerMode {
66 self.mode.unwrap_or(if self.url.is_some() {
67 BrokerMode::ConfiguredOnly
68 } else {
69 BrokerMode::DiscoveryOnly
70 })
71 }
72
73 pub fn requires_tls(&self) -> bool {
75 self.tls || self.url.as_ref().is_some_and(|u| u.broker_type.uses_tls())
76 }
77
78 pub fn accepted(&self) -> Vec<BrokerType> {
82 match &self.accept {
83 Some(a) => {
84 let mut order = Vec::with_capacity(a.len());
85 for t in a {
86 if !order.contains(t) {
87 order.push(*t);
88 }
89 }
90 order
91 }
92 None if self.requires_tls() => alloc::vec![BrokerType::SecureMqtt],
93 None => TCP_BROKER_TYPES.to_vec(),
94 }
95 }
96
97 pub fn check(&self) -> Result<(), BrokerConfigError> {
99 if self.wanted.is_some() && self.url.is_some() {
100 return Err(BrokerConfigError::WantedWithUrl);
101 }
102 if self.effective_mode() == BrokerMode::ConfiguredOnly && self.url.is_none() {
103 return Err(BrokerConfigError::NoUrl);
104 }
105 Ok(())
106 }
107
108 pub fn rank(&self, endpoints: impl IntoIterator<Item = BrokerEndpoint>) -> Vec<BrokerEndpoint> {
113 let order = self.accepted();
114 let sighted: Vec<BrokerEndpoint> = endpoints.into_iter().collect();
115 let wanted = self.wanted.as_ref();
116 rank_brokers(
117 sighted
118 .iter()
119 .filter(|e| wanted.is_none_or(|w| w.matches(e, &sighted)))
120 .cloned(),
121 &order,
122 )
123 }
124}
125
126#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
128#[non_exhaustive]
129pub enum BrokerConfigError {
130 NoUrl,
132 WantedWithUrl,
134}
135
136impl fmt::Display for BrokerConfigError {
137 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138 f.write_str(match self {
139 Self::NoUrl => "configured-only needs a broker URL",
140 Self::WantedWithUrl => "a wanted broker cannot be combined with a broker URL",
141 })
142 }
143}
144
145impl core::error::Error for BrokerConfigError {}
146
147#[derive(Clone, Debug)]
162pub struct BrokerSearch {
163 cfg: BrokerConfig,
164 mode: BrokerMode,
165 table: InstanceTable,
166 examined: BTreeSet<InstanceKey>,
167 settle_at: Option<Instant>,
168 fallback_at: Option<Instant>,
169 requery_at: Option<Instant>,
170 attempt: u32,
171 requery: bool,
172 result: Option<BrokerEndpoint>,
173}
174
175impl BrokerSearch {
176 pub fn new(now: Instant, cfg: BrokerConfig) -> Result<Self, BrokerConfigError> {
179 cfg.check()?;
180 let mode = cfg.effective_mode();
181 let result = match mode {
182 BrokerMode::ConfiguredOnly => cfg.url.clone(),
183 _ => None,
184 };
185 let browses = result.is_none();
186 let fallback_at =
187 (browses && mode == BrokerMode::DiscoveryWithFallback && cfg.url.is_some())
188 .then(|| now.add_ms(cfg.fallback_after_ms));
189 Ok(Self {
190 mode,
191 table: InstanceTable::new(),
192 examined: BTreeSet::new(),
193 settle_at: None,
194 fallback_at,
195 requery_at: browses.then(|| now.add_ms(REQUERY_FAST_MS)),
196 attempt: 0,
197 requery: false,
198 result,
199 cfg,
200 })
201 }
202
203 pub fn browses(&self) -> bool {
205 self.mode != BrokerMode::ConfiguredOnly
206 }
207
208 pub fn mode(&self) -> BrokerMode {
210 self.mode
211 }
212
213 pub fn browse_types(&self) -> &'static [BrokerType] {
215 BROKER_TYPES
216 }
217
218 pub fn handle(&mut self, now: Instant, s: Sighting) {
220 if !self.table.handle(s) || self.result.is_some() {
221 return;
222 }
223 self.log_not_accepted();
224 match self.choice() {
225 Choice::Discovered(..) => {
226 if self.settle_at.is_none() {
227 self.settle_at = Some(now.add_ms(self.cfg.settle_ms));
228 }
229 }
230 Choice::Configured | Choice::None => self.settle_at = None,
231 }
232 }
233
234 pub fn handle_timeout(&mut self, now: Instant) {
236 if self.result.is_some() {
237 return;
238 }
239 if let Some(at) = self.settle_at {
240 if now < at {
241 return;
242 }
243 self.settle_at = None;
244 let ranked = self.candidates();
245 let choice = choose(
246 self.mode,
247 self.cfg.url.as_ref(),
248 &ranked,
249 self.cfg.allow_unmatched,
250 );
251 if let Choice::Discovered(_, ep) = &choice {
252 log_choice(&choice, self.cfg.url.as_ref(), &ranked);
253 crate::log::info!(
254 "brokerDiscovered",
255 url = ep.url(),
256 attempt = self.attempt + 1
257 );
258 self.result = Some(ep.clone());
259 return;
260 }
261 }
262 if let (Some(at), Some(url)) = (self.fallback_at, &self.cfg.url) {
263 if now >= at {
264 crate::log::info!("brokerFallbackToConfigured", url = url.url());
265 self.result = Some(url.clone());
266 return;
267 }
268 }
269 if let Some(at) = self.requery_at {
270 if now >= at {
271 if self.candidates().is_empty() {
272 crate::log::info!("brokerNotFound", attempt = self.attempt + 1);
273 }
274 self.attempt += 1;
275 self.requery = true;
276 let next = if self.attempt + 1 < REQUERY_FAST_ATTEMPTS {
277 REQUERY_FAST_MS
278 } else {
279 REQUERY_SLOW_MS
280 };
281 self.requery_at = Some(now.add_ms(next));
282 }
283 }
284 }
285
286 pub fn poll_requery(&mut self) -> bool {
288 core::mem::take(&mut self.requery)
289 }
290
291 pub fn poll_timeout(&self) -> Option<Instant> {
293 if self.result.is_some() {
294 return None;
295 }
296 if self.settle_at.is_some() {
297 return self.settle_at;
298 }
299 match (self.fallback_at, self.requery_at) {
300 (Some(a), Some(b)) => Some(a.min(b)),
301 (a, b) => a.or(b),
302 }
303 }
304
305 pub fn result(&self) -> Option<&BrokerEndpoint> {
307 self.result.as_ref()
308 }
309
310 pub fn candidates(&self) -> Vec<BrokerEndpoint> {
312 self.cfg.rank(
313 self.table
314 .instances()
315 .filter_map(|i| BrokerEndpoint::from_instance(&i)),
316 )
317 }
318
319 fn choice(&self) -> Choice {
320 choose(
321 self.mode,
322 self.cfg.url.as_ref(),
323 &self.candidates(),
324 self.cfg.allow_unmatched,
325 )
326 }
327
328 fn log_not_accepted(&mut self) {
331 let accept = self.cfg.accepted();
332 let mut usable: Option<Vec<BrokerEndpoint>> = None;
333 for inst in self.table.instances() {
334 if self.examined.contains(&inst.key) {
335 continue;
336 }
337 self.examined.insert(inst.key.clone());
338 let Some(ep) = BrokerEndpoint::from_instance(&inst) else {
339 continue;
340 };
341 if accept.contains(&ep.broker_type) {
342 continue;
343 }
344 let usable = usable.get_or_insert_with(|| {
345 self.cfg.rank(
346 self.table
347 .instances()
348 .filter_map(|i| BrokerEndpoint::from_instance(&i)),
349 )
350 });
351 if !usable.iter().any(|u| crate::same_host(&u.host, &ep.host)) {
352 crate::log::info!(
353 "brokerTransportNotAccepted",
354 url = ep.url(),
355 accept = accept
356 .iter()
357 .map(|t| t.as_str())
358 .collect::<Vec<_>>()
359 .join("|")
360 );
361 }
362 }
363 }
364}
365
366#[cfg(test)]
367#[allow(clippy::field_reassign_with_default)]
369mod tests {
370 use super::*;
371 use crate::addr::ScopedAddr;
372 use crate::instance::Slot;
373 use crate::txt::TxtRecord;
374 use alloc::string::String;
375 use alloc::vec;
376
377 fn seen(t: BrokerType, name: &str, server: &str, txt: &[&str], addrs: &[&str]) -> Sighting {
378 Sighting::Seen {
379 key: InstanceKey {
380 service_type: t.as_str().into(),
381 name: name.into(),
382 },
383 slot: Slot::Whole,
384 server: server.into(),
385 port: 0,
386 txt: TxtRecord::from_strings(txt),
387 addrs: addrs
388 .iter()
389 .map(|a| ScopedAddr::new(a.parse().unwrap()))
390 .collect(),
391 }
392 }
393
394 fn gone(t: BrokerType, name: &str) -> Sighting {
395 Sighting::Gone {
396 key: InstanceKey {
397 service_type: t.as_str().into(),
398 name: name.into(),
399 },
400 slot: None,
401 }
402 }
403
404 fn search(cfg: BrokerConfig) -> BrokerSearch {
405 BrokerSearch::new(Instant(0), cfg).unwrap()
406 }
407
408 fn url(u: &str) -> Option<BrokerEndpoint> {
409 Some(BrokerEndpoint::from_url(u).unwrap())
410 }
411
412 fn result(s: &BrokerSearch) -> Option<String> {
413 s.result().map(BrokerEndpoint::url)
414 }
415
416 fn run(s: &mut BrokerSearch, until: u64) -> Vec<u64> {
418 let mut requeries = Vec::new();
419 while let Some(at) = s.poll_timeout() {
420 if at.0 > until {
421 break;
422 }
423 s.handle_timeout(at);
424 if s.poll_requery() {
425 requeries.push(at.0);
426 }
427 }
428 requeries
429 }
430
431 #[test]
432 fn mode_precedence() {
433 let s = search(BrokerConfig::default());
434 assert_eq!(s.mode(), BrokerMode::DiscoveryOnly);
435 assert!(s.browses());
436 let mut cfg = BrokerConfig::default();
437 cfg.url = url("mqtt://b.local");
438 let s = search(cfg.clone());
439 assert_eq!(s.mode(), BrokerMode::ConfiguredOnly);
440 assert!(!s.browses());
441 assert_eq!(result(&s).as_deref(), Some("mqtt://b.local:1883"));
442 assert_eq!(s.poll_timeout(), None);
443 cfg.mode = Some(BrokerMode::DiscoveryOnly);
444 assert_eq!(search(cfg).mode(), BrokerMode::DiscoveryOnly);
445 }
446
447 #[test]
448 fn config_errors() {
449 let mut cfg = BrokerConfig::default();
450 cfg.mode = Some(BrokerMode::ConfiguredOnly);
451 assert_eq!(
452 BrokerSearch::new(Instant(0), cfg).err(),
453 Some(BrokerConfigError::NoUrl)
454 );
455 for mode in [
456 None,
457 Some(BrokerMode::DiscoveryWithFallback),
458 Some(BrokerMode::DiscoveryOnly),
459 ] {
460 let mut cfg = BrokerConfig::default();
461 cfg.mode = mode;
462 cfg.url = url("mqtt://b.local");
463 cfg.wanted = Some(Wanted::Instance("b".into()));
464 assert_eq!(
465 BrokerSearch::new(Instant(0), cfg).err(),
466 Some(BrokerConfigError::WantedWithUrl)
467 );
468 }
469 }
470
471 #[test]
472 fn discovery_only_settles_then_picks_first() {
473 let mut s = search(BrokerConfig::default());
474 s.handle(
475 Instant(100),
476 seen(BrokerType::Mqtt, "b", "b.local.", &[], &[]),
477 );
478 s.handle(
479 Instant(200),
480 seen(BrokerType::Mqtt, "a", "a.local.", &[], &[]),
481 );
482 assert_eq!(s.poll_timeout(), Some(Instant(3_100)));
483 s.handle_timeout(Instant(3_099));
484 assert_eq!(s.result(), None);
485 assert_eq!(run(&mut s, 3_100), []);
486 assert_eq!(result(&s).as_deref(), Some("mqtt://a.local:1883"));
487 assert_eq!(s.poll_timeout(), None);
488 }
489
490 #[test]
491 fn settle_lets_the_secure_transport_win() {
492 let mut s = search(BrokerConfig::default());
493 s.handle(
494 Instant(0),
495 seen(BrokerType::Mqtt, "b", "b.local.", &[], &[]),
496 );
497 s.handle(
498 Instant(2_000),
499 seen(BrokerType::SecureMqtt, "b", "b.local.", &[], &[]),
500 );
501 assert_eq!(s.poll_timeout(), Some(Instant(3_000)));
502 run(&mut s, 3_000);
503 assert_eq!(result(&s).as_deref(), Some("mqtts://b.local:8883"));
504 assert_eq!(s.candidates().len(), 1);
505 }
506
507 fn plain_first() -> BrokerConfig {
508 let mut cfg = BrokerConfig::default();
509 cfg.accept = Some(vec![BrokerType::Mqtt, BrokerType::SecureMqtt]);
510 cfg
511 }
512
513 #[test]
514 fn client_order_picks_the_plain_transport_of_a_dual_transport_broker() {
515 let mut s = search(plain_first());
516 s.handle(
517 Instant(0),
518 seen(BrokerType::SecureMqtt, "b", "b.local.", &[], &[]),
519 );
520 s.handle(
521 Instant(2_000),
522 seen(BrokerType::Mqtt, "b", "b.local.", &[], &[]),
523 );
524 assert_eq!(s.poll_timeout(), Some(Instant(3_000)));
525 run(&mut s, 3_000);
526 assert_eq!(result(&s).as_deref(), Some("mqtt://b.local:1883"));
527 assert_eq!(s.candidates().len(), 1);
528 }
529
530 #[test]
531 fn client_order_picks_the_plain_broker_of_two() {
532 let mut s = search(plain_first());
533 s.handle(
534 Instant(0),
535 seen(BrokerType::SecureMqtt, "a", "a.local.", &[], &[]),
536 );
537 s.handle(
538 Instant(0),
539 seen(BrokerType::Mqtt, "z", "z.local.", &[], &[]),
540 );
541 run(&mut s, 3_000);
542 assert_eq!(result(&s).as_deref(), Some("mqtt://z.local:1883"));
543 let mut s = search(BrokerConfig::default());
544 s.handle(
545 Instant(0),
546 seen(BrokerType::SecureMqtt, "a", "a.local.", &[], &[]),
547 );
548 s.handle(
549 Instant(0),
550 seen(BrokerType::Mqtt, "z", "z.local.", &[], &[]),
551 );
552 run(&mut s, 3_000);
553 assert_eq!(result(&s).as_deref(), Some("mqtts://a.local:8883"));
554 }
555
556 #[test]
557 fn accepted_keeps_the_first_of_a_repeated_type() {
558 let mut cfg = BrokerConfig::default();
559 cfg.accept = Some(vec![
560 BrokerType::Mqtt,
561 BrokerType::SecureMqtt,
562 BrokerType::Mqtt,
563 ]);
564 assert_eq!(cfg.accepted(), [BrokerType::Mqtt, BrokerType::SecureMqtt]);
565 assert_eq!(BrokerConfig::default().accepted(), TCP_BROKER_TYPES);
566 }
567
568 #[test]
569 fn result_stays_fixed() {
570 let mut s = search(BrokerConfig::default());
571 s.handle(
572 Instant(0),
573 seen(BrokerType::Mqtt, "b", "b.local.", &[], &[]),
574 );
575 run(&mut s, 3_000);
576 s.handle(
577 Instant(4_000),
578 seen(BrokerType::SecureMqtt, "a", "a.local.", &[], &[]),
579 );
580 s.handle(Instant(4_000), gone(BrokerType::Mqtt, "b"));
581 run(&mut s, 100_000);
582 assert_eq!(result(&s).as_deref(), Some("mqtt://b.local:1883"));
583 assert_eq!(s.candidates()[0].host, "a.local");
584 }
585
586 #[test]
587 fn repeated_sightings_do_not_extend_the_settle_window() {
588 let mut s = search(BrokerConfig::default());
589 s.handle(
590 Instant(0),
591 seen(BrokerType::Mqtt, "b", "b.local.", &[], &[]),
592 );
593 s.handle(
594 Instant(2_500),
595 seen(BrokerType::Mqtt, "b", "b.local.", &[], &[]),
596 );
597 s.handle(
598 Instant(2_600),
599 seen(BrokerType::Mqtt, "c", "c.local.", &[], &[]),
600 );
601 assert_eq!(s.poll_timeout(), Some(Instant(3_000)));
602 }
603
604 #[test]
605 fn requery_schedule_while_nothing_is_found() {
606 let mut s = search(BrokerConfig::default());
607 assert_eq!(run(&mut s, 100_000), [3_000, 6_000, 36_000, 66_000, 96_000]);
608 assert!(!s.poll_requery());
609 }
610
611 #[test]
612 fn broker_gone_while_settling_resumes_requeries() {
613 let mut s = search(BrokerConfig::default());
614 s.handle(
615 Instant(1_000),
616 seen(BrokerType::Mqtt, "b", "b.local.", &[], &[]),
617 );
618 s.handle(Instant(3_500), gone(BrokerType::Mqtt, "b"));
619 assert_eq!(s.poll_timeout(), Some(Instant(3_000)));
620 assert_eq!(run(&mut s, 7_000), [3_000, 6_000]);
621 assert_eq!(s.result(), None);
622 }
623
624 #[test]
625 fn tls_narrows_accept() {
626 let mut cfg = BrokerConfig::default();
627 cfg.tls = true;
628 let mut s = search(cfg);
629 s.handle(
630 Instant(0),
631 seen(BrokerType::Mqtt, "p", "p.local.", &[], &[]),
632 );
633 assert!(s.candidates().is_empty());
634 assert_eq!(s.poll_timeout(), Some(Instant(3_000)));
635 s.handle(
636 Instant(1_000),
637 seen(BrokerType::SecureMqtt, "s", "s.local.", &[], &[]),
638 );
639 run(&mut s, 4_000);
640 assert_eq!(result(&s).as_deref(), Some("mqtts://s.local:8883"));
641
642 let mut cfg = BrokerConfig::default();
643 cfg.mode = Some(BrokerMode::DiscoveryWithFallback);
644 cfg.url = url("mqtts://x.local");
645 assert_eq!(cfg.accepted(), [BrokerType::SecureMqtt]);
646 cfg.url = url("mqtt://x.local");
647 assert_eq!(cfg.accepted(), TCP_BROKER_TYPES);
648 cfg.accept = Some(vec![BrokerType::MqttWs]);
649 assert_eq!(cfg.accepted(), [BrokerType::MqttWs]);
650 cfg.tls = true;
651 cfg.accept = Some(vec![BrokerType::Mqtt, BrokerType::SecureMqtt]);
652 assert_eq!(cfg.accepted(), [BrokerType::Mqtt, BrokerType::SecureMqtt]);
653 }
654
655 #[test]
656 fn websocket_brokers_are_not_selected_by_default() {
657 let mut s = search(BrokerConfig::default());
658 s.handle(
659 Instant(0),
660 seen(BrokerType::MqttWss, "w", "w.local.", &[], &[]),
661 );
662 assert!(s.candidates().is_empty());
663 assert_eq!(s.poll_timeout(), Some(Instant(3_000)));
664 }
665
666 #[test]
667 fn wanted_by_instance_and_device_id() {
668 for (wanted, expect) in [
669 (Wanted::Instance("B".into()), "mqtt://b.local:1883"),
670 (Wanted::DeviceId("d2".into()), "mqtts://c.local:8883"),
671 ] {
672 let mut cfg = BrokerConfig::default();
673 cfg.wanted = Some(wanted);
674 let mut s = search(cfg);
675 s.handle(
676 Instant(0),
677 seen(BrokerType::Mqtt, "a", "a.local.", &[], &[]),
678 );
679 s.handle(
680 Instant(0),
681 seen(BrokerType::Mqtt, "b", "b.local.", &[], &[]),
682 );
683 s.handle(
684 Instant(0),
685 seen(
686 BrokerType::SecureMqtt,
687 "c",
688 "c.local.",
689 &["device_id=d1,d2"],
690 &[],
691 ),
692 );
693 run(&mut s, 3_000);
694 assert_eq!(result(&s).as_deref(), Some(expect));
695 }
696 }
697
698 fn device_id_cfg(id: &str, accept: &[BrokerType]) -> BrokerConfig {
699 let mut cfg = BrokerConfig::default();
700 cfg.wanted = Some(Wanted::DeviceId(id.into()));
701 cfg.accept = Some(accept.to_vec());
702 cfg
703 }
704
705 #[test]
706 fn device_id_finds_a_plain_broker_by_its_secure_sibling() {
707 let mut s = search(device_id_cfg("p1", &[BrokerType::Mqtt]));
708 s.handle(
709 Instant(0),
710 seen(
711 BrokerType::SecureMqtt,
712 "c",
713 "c.local.",
714 &["broker=cert.example.com", "device_id=p1"],
715 &[],
716 ),
717 );
718 s.handle(
719 Instant(0),
720 seen(BrokerType::Mqtt, "other", "o.local.", &[], &[]),
721 );
722 s.handle(
723 Instant(0),
724 seen(BrokerType::Mqtt, "c", "c.local.", &[], &["10.0.0.5"]),
725 );
726 run(&mut s, 3_000);
727 assert_eq!(result(&s).as_deref(), Some("mqtt://c.local:1883"));
728 }
729
730 #[test]
731 fn device_id_on_a_plain_record() {
732 let mut s = search(device_id_cfg("p1", &[BrokerType::Mqtt]));
733 s.handle(
734 Instant(0),
735 seen(BrokerType::Mqtt, "a", "a.local.", &[], &[]),
736 );
737 s.handle(
738 Instant(0),
739 seen(BrokerType::Mqtt, "p", "p.local.", &["device_id=p0,p1"], &[]),
740 );
741 run(&mut s, 3_000);
742 assert_eq!(result(&s).as_deref(), Some("mqtt://p.local:1883"));
743 }
744
745 #[test]
746 fn device_id_on_a_websocket_sibling() {
747 let mut s = search(device_id_cfg("p1", &[BrokerType::Mqtt]));
748 s.handle(
749 Instant(0),
750 seen(BrokerType::Mqtt, "c", "c.local.", &[], &[]),
751 );
752 s.handle(
753 Instant(0),
754 seen(BrokerType::MqttWs, "c", "c.local.", &["device_id=p1"], &[]),
755 );
756 run(&mut s, 3_000);
757 assert_eq!(result(&s).as_deref(), Some("mqtt://c.local:1883"));
758 }
759
760 #[test]
761 fn device_id_not_listed_keeps_searching() {
762 let mut s = search(device_id_cfg("p9", &[BrokerType::Mqtt]));
763 s.handle(
764 Instant(0),
765 seen(
766 BrokerType::SecureMqtt,
767 "c",
768 "c.local.",
769 &["device_id=p1"],
770 &[],
771 ),
772 );
773 s.handle(
774 Instant(0),
775 seen(BrokerType::Mqtt, "c", "c.local.", &[], &[]),
776 );
777 assert_eq!(run(&mut s, 7_000), [3_000, 6_000]);
778 assert_eq!(s.result(), None);
779 }
780
781 #[test]
782 fn device_id_never_selects_a_type_not_accepted() {
783 let mut s = search(device_id_cfg("p1", &[BrokerType::Mqtt]));
784 s.handle(
785 Instant(0),
786 seen(
787 BrokerType::SecureMqtt,
788 "c",
789 "c.local.",
790 &["device_id=p1"],
791 &[],
792 ),
793 );
794 assert_eq!(s.poll_timeout(), Some(Instant(3_000)));
796 assert!(s.candidates().is_empty());
797 assert_eq!(run(&mut s, 7_000), [3_000, 6_000]);
798 assert_eq!(s.result(), None);
799 s.handle(
801 Instant(7_000),
802 seen(BrokerType::Mqtt, "c", "c.local.", &[], &[]),
803 );
804 assert_eq!(s.poll_timeout(), Some(Instant(10_000)));
805 run(&mut s, 10_000);
806 assert_eq!(result(&s).as_deref(), Some("mqtt://c.local:1883"));
807 }
808
809 #[test]
810 fn device_id_sibling_opens_the_settle_window_when_it_arrives() {
811 let mut s = search(device_id_cfg("p1", &[BrokerType::Mqtt]));
812 s.handle(
813 Instant(0),
814 seen(BrokerType::Mqtt, "c", "c.local.", &[], &[]),
815 );
816 assert!(s.candidates().is_empty());
817 assert_eq!(s.poll_timeout(), Some(Instant(3_000)));
818 s.handle(
819 Instant(1_000),
820 seen(
821 BrokerType::SecureMqtt,
822 "c",
823 "c.local.",
824 &["device_id=p1"],
825 &[],
826 ),
827 );
828 assert_eq!(s.poll_timeout(), Some(Instant(4_000)));
829 assert_eq!(run(&mut s, 4_000), []);
830 assert_eq!(result(&s).as_deref(), Some("mqtt://c.local:1883"));
831 }
832
833 #[test]
834 fn device_id_finds_the_secure_record_of_a_dual_transport_broker() {
835 let mut cfg = BrokerConfig::default();
836 cfg.wanted = Some(Wanted::DeviceId("d1".into()));
837 let mut s = search(cfg);
838 s.handle(
839 Instant(0),
840 seen(BrokerType::Mqtt, "c", "c.local.", &[], &[]),
841 );
842 s.handle(
843 Instant(0),
844 seen(BrokerType::Mqtt, "plain-only", "p.local.", &[], &[]),
845 );
846 s.handle(
847 Instant(0),
848 seen(
849 BrokerType::SecureMqtt,
850 "c",
851 "c.local.",
852 &["device_id=d1"],
853 &[],
854 ),
855 );
856 run(&mut s, 3_000);
857 assert_eq!(result(&s).as_deref(), Some("mqtts://c.local:8883"));
858 }
859
860 #[test]
861 fn wanted_absent_keeps_searching() {
862 let mut cfg = BrokerConfig::default();
863 cfg.mode = Some(BrokerMode::DiscoveryWithFallback);
864 cfg.wanted = Some(Wanted::Instance("z".into()));
865 let mut s = search(cfg);
866 s.handle(
867 Instant(0),
868 seen(BrokerType::Mqtt, "a", "a.local.", &[], &[]),
869 );
870 assert_eq!(run(&mut s, 40_000), [3_000, 6_000, 36_000]);
871 assert_eq!(s.result(), None);
872 }
873
874 fn fallback_cfg(u: &str) -> BrokerConfig {
875 let mut cfg = BrokerConfig::default();
876 cfg.mode = Some(BrokerMode::DiscoveryWithFallback);
877 cfg.url = url(u);
878 cfg
879 }
880
881 #[test]
882 fn fallback_to_configured() {
883 let mut s = search(fallback_cfg("mqtt://x.local"));
884 s.handle(
885 Instant(0),
886 seen(BrokerType::Mqtt, "a", "a.local.", &[], &[]),
887 );
888 assert_eq!(run(&mut s, 14_999), [3_000, 6_000]);
889 assert_eq!(s.result(), None);
890 run(&mut s, 15_000);
891 assert_eq!(result(&s).as_deref(), Some("mqtt://x.local:1883"));
892 }
893
894 #[test]
895 fn allow_unmatched() {
896 let mut cfg = fallback_cfg("mqtt://x.local");
897 cfg.allow_unmatched = true;
898 let mut s = search(cfg);
899 s.handle(
900 Instant(0),
901 seen(BrokerType::Mqtt, "a", "a.local.", &[], &[]),
902 );
903 run(&mut s, 3_000);
904 assert_eq!(result(&s).as_deref(), Some("mqtt://a.local:1883"));
905 }
906
907 #[test]
908 fn fallback_matches_by_name_and_address() {
909 let mut s = search(fallback_cfg("mqtt://192.168.1.8"));
910 s.handle(
911 Instant(0),
912 seen(BrokerType::Mqtt, "a", "a.local.", &[], &["192.168.1.7"]),
913 );
914 s.handle(
915 Instant(0),
916 seen(BrokerType::Mqtt, "p", "p.local.", &[], &["192.168.1.8"]),
917 );
918 run(&mut s, 3_000);
919 let r = s.result().unwrap();
920 assert_eq!(
921 (r.host.as_str(), r.instance.as_deref()),
922 ("192.168.1.8", Some("p"))
923 );
924
925 let mut s = search(fallback_cfg("mqtts://192.168.1.8:9999"));
926 s.handle(
927 Instant(0),
928 seen(
929 BrokerType::SecureMqtt,
930 "s",
931 "s.local.",
932 &[],
933 &["192.168.1.8"],
934 ),
935 );
936 run(&mut s, 3_000);
937 let r = s.result().unwrap();
938 assert_eq!((r.host.as_str(), r.port), ("s.local", 8883));
939
940 let mut s = search(fallback_cfg("mqtt://B.LOCAL"));
941 s.handle(
942 Instant(0),
943 seen(BrokerType::Mqtt, "b", "b.local.", &[], &[]),
944 );
945 run(&mut s, 3_000);
946 assert_eq!(s.result().unwrap().instance.as_deref(), Some("b"));
947 }
948
949 #[test]
950 fn a_pending_settle_outlasts_the_fallback_deadline() {
951 let mut s = search(fallback_cfg("mqtt://b.local"));
952 s.handle(
953 Instant(14_000),
954 seen(BrokerType::Mqtt, "b", "b.local.", &[], &[]),
955 );
956 assert_eq!(s.poll_timeout(), Some(Instant(17_000)));
957 run(&mut s, 17_000);
958 assert_eq!(s.result().unwrap().instance.as_deref(), Some("b"));
959 }
960
961 #[test]
962 fn device_id_merges_transports_across_hosts() {
963 let mut s = search(BrokerConfig::default());
964 s.handle(
965 Instant(0),
966 seen(
967 BrokerType::SecureMqtt,
968 "x",
969 "x.local.",
970 &["broker=cert.example.com", "device_id=p1"],
971 &[],
972 ),
973 );
974 s.handle(
975 Instant(0),
976 seen(
977 BrokerType::SecureMqtt,
978 "y",
979 "y.local.",
980 &["device_id=p1"],
981 &[],
982 ),
983 );
984 assert_eq!(s.candidates().len(), 1);
985 }
986}