Skip to main content

ebus_homie/controller/
pacing.rs

1//! Subscriptions: the link, stage one and paced stage two (python-sdk#97),
2//! stuck healing, dropped messages, failed commands and timers.
3
4use 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
15/// Nodes without property filters, with their retained property counts, in
16/// declared order. `None` when every node is covered or there is no model;
17/// an empty list when nothing is subscribed and no node is declared.
18fn 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
38/// The next property filters to subscribe: `(None, n)` for the whole device
39/// when nothing is subscribed and its `n` retained properties fit the budget,
40/// else the next nodes in declared order that fit it (at least one).
41fn 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
67/// Forgets property coverage for `nodes` (`None`: the whole device), so the
68/// next chunk subscribes them again.
69fn 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    /// Handles a change in the broker link.
87    ///
88    /// `Up` after a clean session, or after `session_present: true` unless
89    /// [`Config::trust_session`](super::Config::trust_session), queues the
90    /// discovery filter, then stage one for every known device (in tree mode
91    /// the root unpaced, then every tracked descendant), with a fresh
92    /// `$description` required before property filters are paced again; it
93    /// marks every device unconfirmed and arms the absence and sync timers.
94    /// A trusted resumed session keeps everything. [`Event::Link`] follows the
95    /// commands. `Down` while the link is down changes nothing and emits
96    /// nothing.
97    pub fn handle_link(&mut self, now: Instant, link: Link) {
98        self.link(now, link, false);
99    }
100
101    /// [`handle_link`](Self::handle_link). An `implicit` link-up keeps the
102    /// ledger's wire state, since the broker may still hold those filters.
103    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    /// The first link-up: subscribe discovery.
144    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    /// A new session the controller does not trust: re-pace everything.
169    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                // The clean session dropped the descendants' filters as well
222                // (convention.md:259-260); a removed one keeps its attribute
223                // filters so a return is heard.
224                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    /// Stage one: subscribe a device's `$state` (except in wildcard mode) and
255    /// `$description` now; unpaced, its property filters too.
256    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    /// Ends stage one once `$state` and `$description` are held and queues the
302    /// device for its property filters.
303    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    /// Queues a device for stage two when some node lacks property filters.
324    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    /// Moves pending devices into free batch slots and subscribes them.
341    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                // `{n}/+` does not match `$alert/{id}`, which `+/+` carries.
394                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    /// A device's burst is over: free its slot and move on.
413    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    /// Drops a device from pacing, freeing any slot it held.
426    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    /// After a description change, a burst in flight owes what the new
439    /// description declares for its nodes.
440    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    /// Handles every deadline at or before `now`.
463    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    /// Stuck healing (homie.py:4317-4422).
509    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    /// Reports that the host dropped messages for the device `topic` addresses
624    /// (for example on inbox overflow). The next
625    /// [`handle_timeout`](Self::handle_timeout), which a driver calls once its
626    /// backlog drains, resubscribes the device so the broker replays its
627    /// retained messages. In wildcard mode a dropped `$state` also
628    /// resubscribes the discovery filter, which carries every `$state`.
629    ///
630    /// A host that reports a drop only after it has handed later messages to
631    /// the controller calls [`handle_lost`](Self::handle_lost) first, before
632    /// those messages.
633    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    /// Reports, at its place in the message order, that the host dropped a
658    /// message for the device `topic` addresses: no message after it ends a
659    /// re-sync window open for that device, so nothing the drop hid is
660    /// cleared. It heals nothing; [`handle_dropped`](Self::handle_dropped)
661    /// still follows once the backlog drains.
662    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    /// The tracked device a dropped message's `topic` addresses, and whether
671    /// it is a `$state`; `None` for a topic the controller does not want.
672    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    /// The tree root and the single device subscribe their property filters
756    /// with stage one, unpaced.
757    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    /// After stage one's filters were sent again: with the whole device's
765    /// property filters among them, the marker and a window.
766    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    /// Reports a command the host could not hand to its client. A Subscribe
779    /// or Unsubscribe is retried with a 1-30 s backoff; a Publish is dropped
780    /// (`reason=setDropped`), since QoS 0 is never held (convention.md:695).
781    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}