Skip to main content

hick_reactor/
service.rs

1//! Caller-side handle for a registered service.
2
3use std::{
4  collections::VecDeque,
5  sync::{Arc, Mutex, MutexGuard},
6};
7
8use mdns_proto::{ServiceHandle, ServiceUpdate};
9
10use crate::{command::Command, error::CancelError};
11
12/// Upper bound on undelivered NON-terminal service updates buffered per
13/// service.
14///
15/// `Established` is one-time and `Renamed(..)` coalesces to the latest name, so
16/// in normal operation the backlog stays at one or two entries. The cap is a
17/// backstop: an on-link peer can force endless conflict-renames, and a
18/// non-draining caller would otherwise let the buffer grow without bound. Beyond
19/// the cap [`ServiceMailbox::push_update`] drops the oldest pending non-terminal
20/// update. The terminal retirement update (`Conflict`/`HostConflict`) has its
21/// own reserved slot and is never dropped.
22pub(crate) const SERVICE_UPDATE_CAPACITY: usize = 16;
23
24/// Bounded, coalescing delivery buffer shared between the driver task (which
25/// fills it) and the [`Service`] handle (which drains it via [`Service::next`]).
26///
27/// This mirrors [`crate::query::QueryMailbox`]: non-terminal updates are bounded
28/// ([`SERVICE_UPDATE_CAPACITY`]) and coalesced by kind so a flooding peer cannot
29/// grow the queue, while the terminal retirement update keeps a dedicated slot
30/// so it is delivered even under non-terminal backpressure and survives an
31/// immediate ctx GC (the mailbox is owned by the handle, not the driver ctx).
32pub(crate) struct ServiceMailbox {
33  /// NON-terminal updates (`Established` / `Renamed(..)`), coalesced by kind and
34  /// bounded by [`SERVICE_UPDATE_CAPACITY`]. Drained FIFO before the terminal.
35  updates: VecDeque<ServiceUpdate>,
36  /// RESERVED slot for the terminal retirement update (`Conflict` /
37  /// `HostConflict`). Independent of the non-terminal cap and idempotent (first
38  /// terminal wins), so it is always deliverable.
39  terminal: Option<ServiceUpdate>,
40  /// Set once the terminal has been handed to the consumer, so subsequent
41  /// drains report end-of-stream rather than waiting forever.
42  terminal_delivered: bool,
43}
44
45/// Result of draining one update from a [`ServiceMailbox`].
46#[cfg_attr(test, derive(Debug))]
47enum Drained {
48  /// An update is ready for the consumer.
49  Update(ServiceUpdate),
50  /// No more updates will ever arrive (terminal already delivered).
51  Ended,
52  /// Nothing ready right now; the consumer should wait for a wakeup.
53  Empty,
54}
55
56impl ServiceMailbox {
57  fn new() -> Self {
58    Self {
59      updates: VecDeque::new(),
60      terminal: None,
61      terminal_delivered: false,
62    }
63  }
64
65  /// Buffer a NON-terminal update (`Established` / `Renamed(..)`), bounding
66  /// memory while keeping insertion order:
67  ///
68  /// * `Renamed` — drop any prior pending `Renamed` and append the new one, so
69  ///   only the LATEST name is kept, at its true (most recent) position (the
70  ///   caller only needs the current name).
71  /// * `Established` — drop any prior pending `Established` and append, keeping
72  ///   only the LATEST at its most-recent position. A post-rename `Established`
73  ///   therefore lands AFTER the pending `Renamed`, so the caller learns the new
74  ///   name became advertised after a conflict-rename
75  ///   (`Established -> Renamed -> Established`), not merely that a rename
76  ///   happened; a duplicate or pre-rename `Established` coalesces away.
77  ///
78  /// The ring therefore holds at most one `Renamed` plus one trailing
79  /// `Established` under conflict-rename churn; the [`SERVICE_UPDATE_CAPACITY`]
80  /// cap is a hard backstop that drops the oldest pending update if a future
81  /// non-terminal kind ever fills it.
82  ///
83  /// A terminal update passed here is ignored (terminals belong in
84  /// [`Self::set_terminal`]); the driver routes by kind, so this is defensive.
85  pub(crate) fn push_update(&mut self, upd: ServiceUpdate) {
86    if upd.is_conflict() || upd.is_host_conflict() {
87      // Terminals never go into the non-terminal ring; route to the reserved
88      // slot instead so the caller can never lose a retirement reason.
89      self.set_terminal(upd);
90      return;
91    }
92    if upd.is_renamed() {
93      self.updates.retain(|u| !u.is_renamed());
94      self.bounded_push_back(upd);
95      return;
96    }
97    if upd.is_established() {
98      // Keep only the LATEST `Established`, at its most-recent position: drop any
99      // prior `Established`, then append. A post-rename `Established` (the
100      // `Established -> Renamed -> Established` lifecycle: established, conflict,
101      // auto-rename, re-establish under the new name) therefore lands AFTER the
102      // pending `Renamed`, so a slow reader observes "renamed, then established
103      // under the new name" — not just that a rename happened.
104      // Globally deduping by kind instead kept the EARLIER `Established` at the
105      // front and dropped this post-rename one. Bounds the ring to one `Renamed`
106      // plus one trailing `Established` under conflict-rename churn.
107      self.updates.retain(|u| !u.is_established());
108      self.bounded_push_back(upd);
109      return;
110    }
111    // Any future non-terminal kind: append (bounded by the cap).
112    self.bounded_push_back(upd);
113  }
114
115  /// Append `upd`, evicting the oldest pending non-terminal update first if the
116  /// ring is already at capacity (drop-oldest backstop).
117  fn bounded_push_back(&mut self, upd: ServiceUpdate) {
118    if self.updates.len() >= SERVICE_UPDATE_CAPACITY {
119      self.updates.pop_front();
120    }
121    self.updates.push_back(upd);
122  }
123
124  /// Record the terminal retirement update in its reserved slot (idempotent —
125  /// the first terminal wins, and a terminal is never recorded after one has
126  /// already been delivered).
127  pub(crate) fn set_terminal(&mut self, terminal: ServiceUpdate) {
128    if self.terminal.is_none() && !self.terminal_delivered {
129      self.terminal = Some(terminal);
130    }
131  }
132
133  /// Number of buffered NON-terminal updates (excludes the reserved terminal
134  /// slot). Test-only window into the bound + coalesce behaviour.
135  #[cfg(test)]
136  pub(crate) fn non_terminal_len(&self) -> usize {
137    self.updates.len()
138  }
139
140  /// Drain one update (non-terminal first, then the reserved terminal) and
141  /// report it to the caller, or `None` at end-of-stream / when empty. Test-only
142  /// synchronous peek used by the driver-level tests to assert what was
143  /// delivered without awaiting the async [`Service::next`].
144  #[cfg(test)]
145  pub(crate) fn drain_for_test(&mut self) -> Option<ServiceUpdate> {
146    match self.drain() {
147      Drained::Update(upd) => Some(upd),
148      Drained::Ended | Drained::Empty => None,
149    }
150  }
151
152  /// Saturate the NON-terminal ring to [`SERVICE_UPDATE_CAPACITY`] with distinct,
153  /// non-coalescing entries (bypassing `push_update`'s by-kind coalescing).
154  /// Test-only: lets a driver test exercise a FULL non-terminal ring while the
155  /// reserved terminal slot stays independently deliverable.
156  #[cfg(test)]
157  pub(crate) fn fill_non_terminal_to_cap_for_test(&mut self) {
158    use mdns_proto::event::ServiceRenamed;
159    self.updates.clear();
160    for i in 0..SERVICE_UPDATE_CAPACITY {
161      self
162        .updates
163        .push_back(ServiceUpdate::Renamed(ServiceRenamed::new(
164          mdns_proto::Name::try_from_str(&format!("fill-{i}._ipp._tcp.local.")).unwrap(),
165        )));
166    }
167  }
168
169  /// Pull the next update for the consumer: non-terminal updates first (FIFO),
170  /// then the reserved terminal, then end-of-stream.
171  fn drain(&mut self) -> Drained {
172    if let Some(upd) = self.updates.pop_front() {
173      Drained::Update(upd)
174    } else if let Some(terminal) = self.terminal.take() {
175      self.terminal_delivered = true;
176      Drained::Update(terminal)
177    } else if self.terminal_delivered {
178      Drained::Ended
179    } else {
180      Drained::Empty
181    }
182  }
183}
184
185/// Lock the mailbox, recovering the inner guard if a previous holder panicked
186/// (we never hold the lock across a fallible operation, so the data is sound).
187fn lock(mailbox: &Mutex<ServiceMailbox>) -> MutexGuard<'_, ServiceMailbox> {
188  mailbox
189    .lock()
190    .unwrap_or_else(|poisoned| poisoned.into_inner())
191}
192
193/// Handle to a registered service.
194///
195/// Dropping the handle implicitly unregisters the service.
196pub struct Service {
197  handle: ServiceHandle,
198  mailbox: Arc<Mutex<ServiceMailbox>>,
199  /// Capacity-1 wakeup signal: the driver rings it after filling the mailbox.
200  /// Closure of the sender (driver dropped our `ServiceCtx`) means no further
201  /// updates will arrive.
202  doorbell: async_channel::Receiver<()>,
203  cmd: async_channel::Sender<Command>,
204}
205
206impl Service {
207  pub(crate) fn new(
208    handle: ServiceHandle,
209    mailbox: Arc<Mutex<ServiceMailbox>>,
210    doorbell: async_channel::Receiver<()>,
211    cmd: async_channel::Sender<Command>,
212  ) -> Self {
213    Self {
214      handle,
215      mailbox,
216      doorbell,
217      cmd,
218    }
219  }
220
221  /// The underlying proto-layer service handle.
222  #[inline]
223  pub const fn handle(&self) -> ServiceHandle {
224    self.handle
225  }
226
227  /// Wait for the next [`ServiceUpdate`]. Returns `None` once the service has
228  /// been retired (terminal delivered) or the driver task has exited.
229  ///
230  /// Single-consumer: this mirrors [`crate::Query::next`]'s wakeup discipline,
231  /// where the capacity-1 doorbell assumes a single waiter.
232  pub async fn next(&self) -> Option<ServiceUpdate> {
233    loop {
234      match lock(&self.mailbox).drain() {
235        Drained::Update(upd) => return Some(upd),
236        Drained::Ended => return None,
237        Drained::Empty => {}
238      }
239      // Nothing buffered: wait for the driver to ring the doorbell. A closed
240      // doorbell means the driver dropped our context — do one final drain (a
241      // terminal it set just before exiting is still in the mailbox).
242      if self.doorbell.recv().await.is_err() {
243        return match lock(&self.mailbox).drain() {
244          Drained::Update(upd) => Some(upd),
245          _ => None,
246        };
247      }
248    }
249  }
250
251  // an in-place `rename` API was removed because the proto-layer
252  // `Service` exposes no atomic "rename instance" operation. The driver
253  // would have to drop the proto Service and reconstruct one with the new
254  // ServiceSpec, which changes the underlying `ServiceHandle` and forces a
255  // full probing round anyway — better to express that as
256  // `unregister` + `Endpoint::register_service(new_spec).await` at the
257  // caller site so the handle invalidation is explicit.
258  //
259  // The auto-rename path (`ServiceUpdate::Renamed`) is still observed via
260  // `next().await`; the driver keeps the endpoint's route table in sync
261  // before forwarding the event so post-rename queries route correctly.
262
263  /// Explicitly unregister the service. Equivalent to dropping the handle
264  /// but returns an error if the driver task has already exited.
265  pub async fn unregister(self) -> Result<(), CancelError> {
266    self
267      .cmd
268      .send(Command::UnregisterService {
269        handle: self.handle,
270      })
271      .await
272      .map_err(|_| CancelError::DriverGone)?;
273    // The Drop impl below will also try_send an Unregister; driver
274    // tolerates the second one (no-op since the handle is already gone).
275    Ok(())
276  }
277}
278
279impl Drop for Service {
280  fn drop(&mut self) {
281    let _ = self.cmd.try_send(Command::UnregisterService {
282      handle: self.handle,
283    });
284  }
285}
286
287/// Construct a fresh mailbox plus its capacity-1 doorbell channel. Returns the
288/// shared mailbox, the driver-side doorbell sender, and the consumer-side
289/// doorbell receiver. Mirrors [`crate::query::new_mailbox`].
290pub(crate) fn new_service_mailbox() -> (
291  Arc<Mutex<ServiceMailbox>>,
292  async_channel::Sender<()>,
293  async_channel::Receiver<()>,
294) {
295  let mailbox = Arc::new(Mutex::new(ServiceMailbox::new()));
296  let (doorbell_tx, doorbell_rx) = async_channel::bounded(1);
297  (mailbox, doorbell_tx, doorbell_rx)
298}
299
300#[cfg(test)]
301mod tests;