Skip to main content

hick_compio/
query.rs

1//! `Query` handle returned by [`Endpoint::start_query`].
2//!
3//! Holds an [`Rc<EndpointInner>`] + the proto-layer [`QueryHandle`]. The driver
4//! task owns the proto `Query` state machine inside `State.queries`; this handle
5//! reads collected answers and terminal updates directly off the proto state
6//! under a brief borrow, then parks on the shared driver notifier when nothing
7//! is ready.
8//!
9//! Dropping a [`Query`] flags the registration as cancelled and removes it from
10//! the proto endpoint, then wakes the driver so any pending transmits queued for
11//! the handle stop firing.
12//!
13//! [`Endpoint::start_query`]: crate::Endpoint::start_query
14
15use core::cell::Cell;
16use std::rc::Rc;
17
18use mdns_proto::{CollectedAnswer, QueryHandle, QueryUpdate};
19
20use crate::driver::EndpointInner;
21
22/// One event surfaced by [`Query::next`]: either an answer collected by the
23/// underlying state machine, or the terminal update signalling the query has
24/// ended (timeout / done).  Answers are delivered in monotonically-increasing
25/// `seq()` order; the terminal is always the last event surfaced.
26#[derive(Debug, Clone)]
27pub enum QueryEvent {
28  /// A new answer collected by the underlying state machine.
29  Answer(CollectedAnswer),
30  /// The query reached a terminal state. Always the last event delivered.
31  Terminal(QueryUpdate),
32}
33
34/// Handle to an in-flight query.
35///
36/// Dropping the handle implicitly cancels the query: the driver task removes
37/// the proto state machine on its next loop iteration so no further transmits
38/// fire against it.
39pub struct Query {
40  pub(crate) inner: Rc<EndpointInner>,
41  pub(crate) handle: QueryHandle,
42  /// Latches once the terminal `QueryEvent` has been surfaced via
43  /// [`Self::next`], so subsequent calls report end-of-stream rather than
44  /// re-polling the proto layer.  `Cell<bool>` interior mutability is sound
45  /// because the type is `!Send` (held via [`Rc`]) and [`Self::next`] is the
46  /// single consumer.
47  pub(crate) terminal_delivered: Cell<bool>,
48}
49
50impl Query {
51  /// The underlying proto-layer [`QueryHandle`] for this query.
52  #[inline]
53  pub const fn handle(&self) -> QueryHandle {
54    self.handle
55  }
56
57  /// Wait for the next answer or terminal.  Returns `None` once the terminal
58  /// has been delivered (single-consumer; subsequent calls report
59  /// end-of-stream) or the driver has dropped the query entry.
60  ///
61  /// Borrow discipline: every interaction with `inner.state` is confined to a
62  /// short synchronous scope; the only `.await` (`notify.listen()`) runs after
63  /// the borrow is dropped at the closing `}` of the scope.
64  pub async fn next(&self) -> Option<QueryEvent> {
65    loop {
66      if self.terminal_delivered.get() {
67        return None;
68      }
69      // Brief synchronous borrow — read one event off the proto state, else
70      // fall through to park.  The borrow is dropped at the closing `}` of
71      // this block, well before any `.await`.
72      {
73        let mut st = self.inner.state.borrow_mut();
74        // The driver removes our entry on cancellation / endpoint teardown.
75        let ctx = st.queries.get(&self.handle)?;
76        let last = ctx.last_seq;
77        let cancelled = ctx.cancelled;
78        // The driver flags a query `errored` when its question can't be encoded
79        // into `max_payload` (see `QueryCtx::errored`): it has no standing
80        // deadline and can never make progress, so surface end-of-stream rather
81        // than park forever. We still drain any already-collected answers below
82        // first, then fall through to the terminal.
83        let errored = ctx.errored;
84        // Surface the next undelivered answer in `seq` order (see
85        // [`next_answer_by_seq`] for why min-by-seq, not first-iterated),
86        // advancing `last_seq` past it so we don't re-deliver on the next pass.
87        let next_answer = next_answer_by_seq(st.endpoint.collected_answers(self.handle), last);
88        if let Some(a) = next_answer {
89          let new_last = a.seq().saturating_add(1);
90          if let Some(ctx) = st.queries.get_mut(&self.handle) {
91            ctx.last_seq = new_last;
92          }
93          return Some(QueryEvent::Answer(a));
94        }
95        // No pending answer; the proto layer surfaces the terminal once via
96        // `poll_query`, then None forever.
97        if let Some(t) = st.endpoint.poll_query(self.handle) {
98          self.terminal_delivered.set(true);
99          // GC: free the driver query slot and the proto pool entry now that
100          // the terminal has been observed.  The Drop impl is safe to run
101          // afterward: `cancel_query` is idempotent (returns QueryNotFound and
102          // is ignored), and `queries.remove` on a missing key is a no-op.
103          st.queries.remove(&self.handle);
104          let _ = st.endpoint.cancel_query(self.handle);
105          return Some(QueryEvent::Terminal(t));
106        }
107        // Driver flagged us cancelled (drop path) or errored (un-encodable
108        // question) and no answer/terminal was pending — surface end-of-stream.
109        if cancelled || errored {
110          self.terminal_delivered.set(true);
111          // GC: same cleanup as the Terminal branch above.
112          st.queries.remove(&self.handle);
113          let _ = st.endpoint.cancel_query(self.handle);
114          return None;
115        }
116      }
117      // Nothing buffered: park on the driver's notify. The borrow above is
118      // already dropped, so this await never holds a RefCell borrow.
119      self.inner.notify.listen().await;
120    }
121  }
122}
123
124/// Pick the next answer to surface from a query's collected-answer snapshot:
125/// the one with the SMALLEST `seq` that is `>= last` (the smallest not-yet-
126/// delivered sequence number), cloned out.
127///
128/// Selecting the minimum — rather than the first item the iterator yields — is
129/// load-bearing. `collected_answers` is proto's BOUNDED snapshot backed by a
130/// `slab::Slab`, so it iterates in SLAB-KEY order, not `seq` order: once
131/// `max_answers` is reached the proto FIFO-evicts the lowest-`seq` entry and the
132/// freed low slab key is reused by the next insert, so a higher-`seq` answer can
133/// sit at a lower key ahead of older retained lower-`seq` answers. Taking the
134/// first item with `seq >= last` (the previous behaviour) would deliver that
135/// higher-`seq` answer, jump `last_seq` past it, and PERMANENTLY skip the still-
136/// retained lower-`seq` answers — so a peer flooding more than `max_answers`
137/// distinct records before the app drains could make browse/lookup/resolve
138/// silently miss valid records. Min-by-`seq` keeps delivery strictly ordered
139/// regardless of slab layout.
140// `'a` reads as single-use, but it cannot be elided at our 1.91 MSRV:
141// anonymous lifetimes in `impl Trait` are unstable there (rustc E0658).
142#[allow(single_use_lifetimes)]
143fn next_answer_by_seq<'a>(
144  answers: impl Iterator<Item = &'a CollectedAnswer>,
145  last: u64,
146) -> Option<CollectedAnswer> {
147  answers
148    .filter(|a| a.seq() >= last)
149    .min_by_key(|a| a.seq())
150    .cloned()
151}
152
153impl Drop for Query {
154  fn drop(&mut self) {
155    // Flag cancellation, then remove the proto-layer query immediately so any
156    // pending transmits stop firing.  Wake the driver so it observes the
157    // change on its next loop iteration.
158    {
159      let mut st = self.inner.state.borrow_mut();
160      st.flag_query_cancelled(self.handle);
161      let _ = st.endpoint.cancel_query(self.handle);
162      st.queries.remove(&self.handle);
163    }
164    // Durable wake (see `EndpointInner::dirty`): removing the query may free the
165    // last handle, and the driver must re-settle to observe the change even if a
166    // bare notify would be lost across its send-awaits.
167    self.inner.mark_dirty();
168  }
169}
170
171#[cfg(test)]
172mod tests;