1use alloc::{collections::BTreeSet, string::String, vec::Vec};
5
6use ebus_mqtt::{Command, Instant, Link};
7
8use super::key::DevKey;
9use super::replay::{abandon, holds_any};
10use super::{
11 ALERTS, Attrs, Burst, Controller, Coverage, Device, Discovery, Event, Id, Missing,
12 PendingEntry, RETRY_MAX_MS, RETRY_MIN_MS,
13};
14
15fn uncovered(dev: &Device) -> Option<Vec<(Id, u64)>> {
19 let desc = dev.view.description.as_ref()?;
20 let covered = match &dev.coverage {
21 Coverage::All => return None,
22 Coverage::None => None,
23 Coverage::Nodes(s) => Some(s),
24 };
25 let mut out = Vec::new();
26 for (nid, _) in &desc.nodes {
27 let Some((id, m)) = dev.view.props.get_key_value(nid.as_str()) else {
28 continue;
29 };
30 if covered.is_some_and(|s| s.contains(id)) {
31 continue;
32 }
33 out.push((id.clone(), m.values().filter(|s| s.retained).count() as u64));
34 }
35 (covered.is_none() || !out.is_empty()).then_some(out)
36}
37
38fn next_chunk(dev: &Device, budget: u64) -> Option<(Option<Vec<Id>>, u64)> {
42 let nodes = uncovered(dev)?;
43 let total: u64 = nodes.iter().map(|(_, c)| c).sum();
44 if dev.coverage == Coverage::None && 2 * total <= budget {
45 return Some((None, total));
46 }
47 let mut chunk = Vec::new();
48 let mut expected = 0u64;
49 for (n, c) in nodes {
50 if !chunk.is_empty() && 2 * (expected + c) > budget {
51 break;
52 }
53 chunk.push(n);
54 expected += c;
55 }
56 Some((Some(chunk), expected))
57}
58
59fn clear_burst(dev: &mut Device) {
60 for m in dev.marks.values_mut() {
61 for s in m.values_mut() {
62 s.burst = 0;
63 }
64 }
65}
66
67fn uncover(dev: &mut Device, nodes: Option<&[Id]>) {
70 match nodes {
71 None => dev.coverage = Coverage::None,
72 Some(ns) => {
73 if let Coverage::Nodes(s) = &mut dev.coverage {
74 for n in ns {
75 s.remove(n);
76 }
77 if s.is_empty() {
78 dev.coverage = Coverage::None;
79 }
80 }
81 }
82 }
83}
84
85impl Controller {
86 pub fn handle_link(&mut self, now: Instant, link: Link) {
98 self.link(now, link, false);
99 }
100
101 pub(super) fn link(&mut self, now: Instant, link: Link, implicit: bool) {
104 if self.shut_down {
105 return;
106 }
107 match link {
108 Link::Down => {
109 if !self.connected {
110 return;
111 }
112 crate::log::info!("controllerDisconnect");
113 self.connected = false;
114 }
115 Link::Up {
116 session_present, ..
117 } => {
118 let first = !self.ever_up;
119 self.ever_up = true;
120 self.connected = true;
121 self.backoff_ms = RETRY_MIN_MS;
122 self.ledger.link_up(session_present || implicit);
123 self.replay_link_up(session_present || implicit);
124 crate::log::info!("controllerOnConnect", session_present = session_present);
125 if first {
126 self.start(now);
127 } else if session_present && self.cfg.trust_session {
128 for a in self.awaiting.values_mut() {
129 a.subscribed_at = now;
130 }
131 for b in self.draining.values_mut() {
132 b.subscribed_at = now;
133 }
134 } else {
135 self.repace(now);
136 }
137 }
138 _ => return,
139 }
140 self.events.push_back(Event::Link(link));
141 }
142
143 fn start(&mut self, now: Instant) {
145 match self.cfg.discovery.clone() {
146 Discovery::Wildcard => {
147 if let Some(f) = self.discovery_filter.clone() {
148 crate::log::info!("startDiscovery", topic = f);
149 let cmd = self
150 .ledger
151 .subscribe(alloc::vec![(f, self.cfg.subscribe_qos)]);
152 self.push(cmd);
153 }
154 }
155 Discovery::Tree { root: id } | Discovery::Device(id) => {
156 let Some(key) = self.named_key(&id) else {
157 return;
158 };
159 crate::log::info!("startDiscovery", device = id);
160 self.devices
161 .entry(key.clone())
162 .or_insert_with(|| Device::new(&key));
163 self.request_subscription(now, &key, false, false);
164 }
165 }
166 }
167
168 fn repace(&mut self, now: Instant) {
170 self.awaiting.clear();
171 self.pending.clear();
172 self.pending_entries.clear();
173 self.draining.clear();
174 self.retries.clear();
175 self.dropped.clear();
176 self.dropped_discovery = false;
177 self.dropped_due = None;
178 let keys: Vec<DevKey> = self.holds.keys().cloned().collect();
179 for k in &keys {
180 self.release_props(k);
181 }
182 let sync_ms = self.cfg.sync_timeout_ms;
183 for d in self.devices.values_mut() {
184 d.coverage = Coverage::None;
185 clear_burst(d);
186 d.view.confirmed = false;
187 d.absent = false;
188 if d.view.description.is_some() {
189 d.view.synced = false;
190 d.sync_at = Some(now.add_ms(sync_ms));
191 d.sync_expired = false;
192 }
193 }
194 self.floor_sync(now.add_ms(sync_ms));
195 if let Some(f) = self.discovery_filter.clone() {
196 self.resubscribe(&[f]);
197 }
198 match self.cfg.discovery.clone() {
199 Discovery::Wildcard => {
200 let keys: Vec<DevKey> = self
201 .devices
202 .iter()
203 .filter(|(_, d)| d.view.state.is_some())
204 .map(|(k, _)| k.clone())
205 .collect();
206 if !keys.is_empty() {
207 crate::log::info!("resyncRepaceDevices", devices = keys.len());
208 }
209 for k in &keys {
210 self.request_subscription(now, k, true, true);
211 }
212 }
213 Discovery::Tree { root: id } => {
214 let Some(key) = self.named_key(&id) else {
215 return;
216 };
217 self.devices
218 .entry(key.clone())
219 .or_insert_with(|| Device::new(&key));
220 self.request_subscription(now, &key, false, true);
221 let tracked: Vec<DevKey> = self
225 .devices
226 .keys()
227 .filter(|k| **k != key)
228 .cloned()
229 .collect();
230 for k in &tracked {
231 self.request_subscription(now, k, true, true);
232 }
233 let removed: Vec<String> = self
234 .holds
235 .iter()
236 .filter(|(k, _)| !self.devices.contains_key(*k))
237 .flat_map(|(_, h)| h.attrs.iter().cloned())
238 .collect();
239 self.resubscribe(&removed);
240 }
241 Discovery::Device(id) => {
242 let Some(key) = self.named_key(&id) else {
243 return;
244 };
245 self.devices
246 .entry(key.clone())
247 .or_insert_with(|| Device::new(&key));
248 self.request_subscription(now, &key, false, true);
249 }
250 }
251 self.absent_due = Some(now.add_ms(self.cfg.absent_after_ms));
252 }
253
254 pub(super) fn request_subscription(
257 &mut self,
258 now: Instant,
259 key: &DevKey,
260 paced: bool,
261 refresh: bool,
262 ) {
263 if self.in_flight(key) {
264 return;
265 }
266 let attrs = self.attr_filters(key);
267 let unpaced = !paced || self.cfg.pacing.is_none();
268 let window = unpaced
269 && !self.no_retain_bit
270 && self
271 .devices
272 .get(key)
273 .is_some_and(|d| holds_any(d, None, true));
274 let props = if unpaced {
275 if let Some(d) = self.devices.get_mut(key) {
276 d.coverage = Coverage::All;
277 }
278 Self::prop_filters(key, None)
279 } else {
280 Vec::new()
281 };
282 let mut filters = attrs.clone();
283 filters.extend(props.iter().cloned());
284 self.floor_stuck(now);
285 self.awaiting.insert(
286 key.clone(),
287 Attrs {
288 filters,
289 subscribed_at: now,
290 attempts: 0,
291 refresh,
292 props: unpaced,
293 },
294 );
295 self.hold(key, attrs, props, false);
296 if window {
297 self.mark_and_open(key, None, true);
298 }
299 }
300
301 pub(super) fn note_progress(&mut self, now: Instant, key: &DevKey) {
304 let Some(dev) = self.devices.get(key) else {
305 return;
306 };
307 if dev.view.state.is_none() || dev.raw_hash.is_none() {
308 return;
309 }
310 if self.awaiting.get(key).is_some_and(|a| a.refresh) {
311 return;
312 }
313 self.awaiting.remove(key);
314 if self.cfg.pacing.is_some() {
315 self.requeue_uncovered(key, false);
316 if self.pending_entries.contains_key(key) {
317 self.take_pending(now);
318 }
319 }
320 self.check_sync(now, key);
321 }
322
323 fn requeue_uncovered(&mut self, key: &DevKey, front: bool) {
325 let Some(dev) = self.devices.get(key) else {
326 return;
327 };
328 if dev.view.state.is_none() || self.in_flight(key) || uncovered(dev).is_none() {
329 return;
330 }
331 if front {
332 self.pending.push_front(key.clone());
333 } else {
334 self.pending.push_back(key.clone());
335 }
336 self.pending_entries
337 .insert(key.clone(), PendingEntry::default());
338 }
339
340 pub(super) fn take_pending(&mut self, now: Instant) {
342 let Some(p) = self.cfg.pacing else {
343 return;
344 };
345 let budget = p.batch_values as u64;
346 let mut owed: u64 = self.draining.values().map(Burst::owed).sum();
347 while self.draining.len() < p.batch_devices {
348 let Some(key) = self.pending.front().cloned() else {
349 break;
350 };
351 let Some((nodes, expected)) =
352 self.devices.get(&key).and_then(|d| next_chunk(d, budget))
353 else {
354 self.pending.pop_front();
355 self.pending_entries.remove(&key);
356 continue;
357 };
358 if !self.draining.is_empty() && owed + 2 * expected > budget {
359 break;
360 }
361 self.pending.pop_front();
362 let entry = self.pending_entries.remove(&key).unwrap_or_default();
363 owed += 2 * expected;
364 let Some(dev) = self.devices.get_mut(&key) else {
365 continue;
366 };
367 let first_chunk = nodes.is_some() && dev.coverage == Coverage::None;
368 match (&nodes, &mut dev.coverage) {
369 (None, c) => *c = Coverage::All,
370 (Some(ns), Coverage::Nodes(s)) => s.extend(ns.iter().cloned()),
371 (Some(ns), c) => *c = Coverage::Nodes(ns.iter().cloned().collect::<BTreeSet<_>>()),
372 }
373 let marked = expected > 0;
374 let alerts = nodes.is_none() || first_chunk;
375 let window = !self.no_retain_bit && holds_any(dev, nodes.as_deref(), alerts);
376 if marked {
377 clear_burst(dev);
378 self.draining.insert(
379 key.clone(),
380 Burst {
381 nodes: nodes.clone(),
382 expected: u32::try_from(expected).unwrap_or(u32::MAX),
383 received: 0,
384 subscribed_at: now,
385 attempts: entry.attempts,
386 last_received: entry.last_received,
387 },
388 );
389 self.floor_stuck(now);
390 }
391 let mut filters = Self::prop_filters(&key, nodes.as_deref());
392 if first_chunk {
393 filters.push(Self::device_filter(&key, ALERTS));
395 }
396 crate::log::info!(
397 "pacedSubscribe",
398 device = key.id,
399 filters = filters.len(),
400 pending = self.pending.len()
401 );
402 self.hold(&key, Vec::new(), filters, marked || window);
403 if window {
404 self.open_window(&key, nodes.clone(), alerts);
405 }
406 if !marked {
407 self.requeue_uncovered(&key, true);
408 }
409 }
410 }
411
412 pub(super) fn release_slot(&mut self, now: Instant, key: &DevKey) {
414 if self.draining.remove(key).is_none() {
415 return;
416 }
417 if let Some(d) = self.devices.get_mut(key) {
418 clear_burst(d);
419 }
420 self.requeue_uncovered(key, true);
421 self.take_pending(now);
422 self.check_sync(now, key);
423 }
424
425 pub(super) fn forget(&mut self, now: Instant, key: &DevKey) {
427 self.awaiting.remove(key);
428 let mut freed = self.draining.remove(key).is_some();
429 if self.pending_entries.remove(key).is_some() {
430 self.pending.retain(|k| k != key);
431 freed = true;
432 }
433 if freed {
434 self.take_pending(now);
435 }
436 }
437
438 pub(super) fn recount_burst(&mut self, now: Instant, key: &DevKey) {
441 let (Some(b), Some(dev)) = (self.draining.get_mut(key), self.devices.get(key)) else {
442 return;
443 };
444 let (mut expected, mut received) = (0u32, 0u32);
445 for (node, m) in dev.view.props.iter() {
446 if !b.covers(node.as_str()) {
447 continue;
448 }
449 for s in m.values().filter(|s| s.retained) {
450 let burst = dev.marks(&s.node, &s.prop).burst;
451 expected += 1;
452 received += u32::from(burst & 1) + u32::from(burst >> 1 & 1);
453 }
454 }
455 b.expected = expected;
456 b.received = received;
457 if b.owed() == 0 {
458 self.release_slot(now, key);
459 }
460 }
461
462 pub fn handle_timeout(&mut self, now: Instant) {
464 if self.shut_down {
465 return;
466 }
467 if self.connected {
468 self.run_retries(now);
469 if self.dropped_due.is_some_and(|t| t <= now) {
470 self.heal_dropped(now);
471 }
472 self.check_stuck(now);
473 if self.absent_due.is_some_and(|t| t <= now) {
474 self.absent_due = None;
475 for (k, d) in self.devices.iter_mut() {
476 if d.view.state.is_some() && !d.view.confirmed && !d.absent {
477 d.absent = true;
478 crate::log::info!("deviceAbsent", device = k.id);
479 self.events.push_back(Event::DeviceAbsent {
480 device: k.id.clone(),
481 });
482 }
483 }
484 }
485 }
486 let due: Vec<DevKey> = self
487 .devices
488 .iter()
489 .filter(|(_, d)| d.sync_at.is_some_and(|t| t <= now))
490 .map(|(k, _)| k.clone())
491 .collect();
492 for key in due {
493 if self.in_flight(&key) {
494 if let Some(d) = self.devices.get_mut(&key) {
495 d.sync_at = None;
496 d.sync_expired = true;
497 }
498 } else {
499 self.check_sync(now, &key);
500 if let Some(d) = self.devices.get_mut(&key) {
501 d.sync_at = None;
502 }
503 }
504 }
505 self.recompute_floors();
506 }
507
508 fn check_stuck(&mut self, now: Instant) {
510 let Some(timeout) = self.cfg.stuck_timeout_ms else {
511 return;
512 };
513 let max = self.cfg.max_resubscribe_attempts;
514 let due: Vec<DevKey> = self
515 .awaiting
516 .iter()
517 .filter(|(_, a)| now.saturating_since(a.subscribed_at) >= timeout)
518 .map(|(k, _)| k.clone())
519 .collect();
520 for key in due {
521 let refresh = self.awaiting.get(&key).is_some_and(|a| a.refresh);
522 let missing = match self.devices.get(&key) {
523 None => {
524 self.awaiting.remove(&key);
525 continue;
526 }
527 Some(d) if d.view.state.is_none() => Missing::State,
528 Some(d) if d.raw_hash.is_none() || refresh => Missing::Description,
529 Some(_) => {
530 self.awaiting.remove(&key);
531 self.note_progress(now, &key);
532 continue;
533 }
534 };
535 let Some(a) = self.awaiting.get_mut(&key) else {
536 continue;
537 };
538 if a.attempts >= max {
539 let attempt = a.attempts;
540 self.awaiting.remove(&key);
541 crate::log::error!("stuckDeviceGaveUp", device = key.id, attempts = attempt);
542 self.events.push_back(Event::DeviceStuck {
543 device: key.id.clone(),
544 missing,
545 attempt,
546 gave_up: true,
547 });
548 continue;
549 }
550 a.attempts += 1;
551 a.subscribed_at = now;
552 let attempt = a.attempts;
553 let filters = a.filters.clone();
554 let props = a.props;
555 crate::log::warn!("stuckDeviceResubscribe", device = key.id, attempt = attempt);
556 self.resubscribe(&filters);
557 self.mark_if_held(&key, props);
558 self.events.push_back(Event::DeviceStuck {
559 device: key.id.clone(),
560 missing,
561 attempt,
562 gave_up: false,
563 });
564 }
565
566 let due: Vec<DevKey> = self
567 .draining
568 .iter()
569 .filter(|(_, b)| now.saturating_since(b.subscribed_at) >= timeout)
570 .map(|(k, _)| k.clone())
571 .collect();
572 let mut gave_up = Vec::new();
573 for key in due {
574 let Some(b) = self.draining.remove(&key) else {
575 continue;
576 };
577 let Some(dev) = self.devices.get_mut(&key) else {
578 continue;
579 };
580 clear_burst(dev);
581 let stalled = b.last_received.is_some_and(|l| b.received <= l);
582 if b.attempts >= max || stalled {
583 crate::log::error!(
584 "stuckDeviceGaveUp",
585 device = key.id,
586 received = b.received,
587 attempts = b.attempts
588 );
589 self.events.push_back(Event::DeviceStuck {
590 device: key.id.clone(),
591 missing: Missing::Properties,
592 attempt: b.attempts,
593 gave_up: true,
594 });
595 self.requeue_uncovered(&key, false);
596 gave_up.push(key);
597 continue;
598 }
599 uncover(dev, b.nodes.as_deref());
600 let attempt = b.attempts.saturating_add(1);
601 self.pending.push_back(key.clone());
602 self.pending_entries.insert(
603 key.clone(),
604 PendingEntry {
605 attempts: attempt,
606 last_received: Some(b.received),
607 },
608 );
609 crate::log::warn!("stuckDeviceRequeue", device = key.id, attempt = attempt);
610 self.events.push_back(Event::DeviceStuck {
611 device: key.id.clone(),
612 missing: Missing::Properties,
613 attempt,
614 gave_up: false,
615 });
616 }
617 self.take_pending(now);
618 for key in gave_up {
619 self.check_sync(now, &key);
620 }
621 }
622
623 pub fn handle_dropped(&mut self, now: Instant, topic: &str) {
634 let Some((key, state)) = self.lost_key(topic) else {
635 return;
636 };
637 let discovery = state && self.discovery_filter.is_some();
638 match key {
639 Some(k) => {
640 if let Some(d) = self.devices.get_mut(&k) {
641 abandon(d);
642 }
643 self.dropped.insert(k);
644 }
645 None if discovery => {}
646 None => return,
647 }
648 if discovery {
649 self.dropped_discovery = true;
650 }
651 crate::log::warn!("messagesDropped", topic = topic);
652 if self.dropped_due.is_none() {
653 self.dropped_due = Some(now);
654 }
655 }
656
657 pub fn handle_lost(&mut self, topic: &str) {
663 if let Some((Some(k), _)) = self.lost_key(topic) {
664 if let Some(d) = self.devices.get_mut(&k) {
665 abandon(d);
666 }
667 }
668 }
669
670 fn lost_key(&self, topic: &str) -> Option<(Option<DevKey>, bool)> {
673 if self.shut_down || !self.wants(topic) {
674 return None;
675 }
676 let t = crate::topic::parse(topic).ok()?;
677 let id = t.device?;
678 let state = matches!(t.kind, crate::topic::TopicKind::DeviceState);
679 Some((self.key_of(id, t.domain), state))
680 }
681
682 fn heal_dropped(&mut self, now: Instant) {
683 self.dropped_due = None;
684 let keys = core::mem::take(&mut self.dropped);
685 for key in &keys {
686 if !self.devices.contains_key(key) {
687 continue;
688 }
689 crate::log::info!("droppedResubscribe", device = key.id);
690 if let Some(a) = self.awaiting.get_mut(key) {
691 a.subscribed_at = now;
692 let filters = a.filters.clone();
693 let props = a.props;
694 self.resubscribe(&filters);
695 self.mark_if_held(key, props);
696 continue;
697 }
698 if self.pending_entries.contains_key(key) {
699 continue;
700 }
701 if let Some(b) = self.draining.remove(key) {
702 if let Some(dev) = self.devices.get_mut(key) {
703 clear_burst(dev);
704 uncover(dev, b.nodes.as_deref());
705 }
706 self.pending.push_back(key.clone());
707 self.pending_entries.insert(
708 key.clone(),
709 PendingEntry {
710 attempts: b.attempts,
711 last_received: None,
712 },
713 );
714 continue;
715 }
716 let (attrs, props) = self
717 .holds
718 .get(key)
719 .map(|h| (h.attrs.clone(), h.props.clone()))
720 .unwrap_or_default();
721 self.resubscribe(&attrs);
722 let paced = self.cfg.pacing.is_some()
723 && !self.is_unpaced_device(key)
724 && self
725 .devices
726 .get(key)
727 .is_some_and(|d| d.view.description.is_some());
728 if paced {
729 if let Some(dev) = self.devices.get_mut(key) {
730 dev.coverage = Coverage::None;
731 }
732 self.requeue_uncovered(key, false);
733 } else if !self.no_retain_bit
734 && self
735 .devices
736 .get(key)
737 .is_some_and(|d| holds_any(d, None, true))
738 {
739 let mut filters = props;
740 filters.push(Self::device_filter(key, "$description"));
741 self.resubscribe(&filters);
742 self.open_window(key, None, true);
743 } else {
744 self.resubscribe(&props);
745 }
746 }
747 if core::mem::take(&mut self.dropped_discovery) {
748 if let Some(f) = self.discovery_filter.clone() {
749 self.resubscribe(&[f]);
750 }
751 }
752 self.take_pending(now);
753 }
754
755 fn is_unpaced_device(&self, key: &DevKey) -> bool {
758 match &self.cfg.discovery {
759 Discovery::Wildcard => false,
760 Discovery::Tree { root: id } | Discovery::Device(id) => *id == key.id,
761 }
762 }
763
764 fn mark_if_held(&mut self, key: &DevKey, props: bool) {
767 if props
768 && !self.no_retain_bit
769 && self
770 .devices
771 .get(key)
772 .is_some_and(|d| holds_any(d, None, true))
773 {
774 self.mark_and_open(key, None, true);
775 }
776 }
777
778 pub fn handle_command_failed(&mut self, now: Instant, cmd: Command) {
782 if self.shut_down {
783 return;
784 }
785 match cmd {
786 Command::Publish { topic, .. } => {
787 crate::log::warn!("setDropped", topic = topic);
788 }
789 c @ (Command::Subscribe { .. } | Command::Unsubscribe { .. }) => {
790 if let Command::Subscribe { filters, .. } = &c {
791 self.unnumber_subscribe(filters);
792 }
793 crate::log::warn!("commandRetry", delay_ms = self.backoff_ms);
794 self.retries.push((now.add_ms(self.backoff_ms), c));
795 self.backoff_ms = (self.backoff_ms * 2).min(RETRY_MAX_MS);
796 }
797 _ => {
798 crate::log::debug!("commandFailedIgnored");
799 }
800 }
801 }
802
803 fn run_retries(&mut self, now: Instant) {
804 if !self.retries.iter().any(|(t, _)| *t <= now) {
805 return;
806 }
807 let (due, later): (Vec<_>, Vec<_>) = core::mem::take(&mut self.retries)
808 .into_iter()
809 .partition(|(t, _)| *t <= now);
810 self.retries = later;
811 for (_, cmd) in due {
812 match cmd {
813 Command::Subscribe { filters, .. } => {
814 let cmd = self.ledger.resubscribe(filters.iter().map(|(f, _)| f));
815 self.push(cmd);
816 }
817 Command::Unsubscribe { filters, .. } => {
818 let filters: Vec<_> = filters
819 .into_iter()
820 .filter(|f| {
821 self.ledger.count(f) == 0 && !self.ledger.is_host_filter(f) && f != "#"
822 })
823 .collect();
824 if !filters.is_empty() {
825 self.commands.push_back(Command::unsubscribe(filters));
826 }
827 }
828 _ => {}
829 }
830 }
831 }
832}