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;