Skip to main content

godspeed/
call.rs

1// SPDX-License-Identifier: Apache-2.0
2//! One way to ask a service for something, and get an honest answer.
3//!
4//! # Why this exists
5//!
6//! The SDK offers twelve `request_with_reply*` functions across three outcome enums. They are all
7//! correct; the problem is which one is shortest to type. `request_with_reply_deadline` returns
8//! `Option<Message>`, so "the request never left" and "the deadline passed" arrive as the same
9//! `None` - and those two demand opposite responses. Services that needed the distinction used to
10//! reach for the longer `_call_err` variant to get it back; since every service moved onto this
11//! library (2026-10-09), none calls `_call_err` directly - they call [`request_within`], which rides it.
12//!
13//! This module offers ONE way to ask, in four shapes: [`request`] and [`request_within`] (the default,
14//! and the one to reach for), [`request_once`] (the same with no retry, for a caller that owns its own
15//! recovery) and [`request_within_notice`] (for a person waiting at a prompt). The default keeps the
16//! distinction and does the one retry that is provably safe, so that the easy path and the correct
17//! path are the same path.
18//!
19//! # What it does on your behalf, and what it refuses to
20//!
21//! ```text
22//!   send ──▶ Reply          you get the message
23//!        │
24//!        ├─▶ SendFailed ──▶ REACQUIRE the peer by name, send ONCE more
25//!        │                  (§14.3: a cap to a restarted service is stale forever,
26//!        │                   and the obligation to notice is the client's)
27//!        │                     └─▶ still failing: Err(Unreachable)  nothing happened
28//!        │
29//!        ├─▶ QueueFull ────▶ Err(Busy)            nothing happened; pace yourself
30//!        │
31//!        ├─▶ ReplyDead ────▶ Err(PeerDied)        IT MAY HAVE HAPPENED; the peer is gone
32//!        │
33//!        └─▶ Timeout ──────▶ Err(OutcomeUnknown)  IT MAY HAVE HAPPENED
34//! ```
35//!
36//! **It never retries a timeout, or a peer that died holding the request.** That is not caution, it
37//! is correctness: a slow service is a live service, and re-sending a `delete` to one is not a retry,
38//! it is a second delete whose failure looks like success. `ReplyDead` says the request was DELIVERED
39//! (CLAUDE.md 8.6), so it is the same case with a dead peer instead of a slow one.
40//!
41//! This diagram is [`request_within`]. [`request_within_notice`] with a callback follows it with two
42//! differences - a peer that died holding the request reads as `OutcomeUnknown`, and a person can
43//! press `q` - which its own documentation explains.
44//!
45//! # ONE OF THESE IS SAFE FOR A SERVING CALLER, AND THE OTHER IS NOT
46//!
47//! Read this before picking. The difference is not ergonomic, it decides whether a task that also
48//! SERVES clients can lose one of their requests (CLAUDE.md 8.2).
49//!
50//! - [`request_within`] rides `CallDeadline`. The kernel matches the reply to this call's own
51//!   one-shot reply capability and leaves every other message queued. **Safe for a serving task.**
52//! - [`request_within_notice`] rides the SDK's `_qhint` variant, which DRAINS the endpoint before
53//!   sending (`while self.try_recv().is_some() {}`) and then waits with a plain timed receive, taking
54//!   whatever lands next. **NOT safe for a serving task**: a client request arriving mid-wait is
55//!   consumed and dropped.
56//!
57//! That is deliberate rather than an oversight, and the SDK says so where it lives: `_qhint`
58//! interleaves ON PURPOSE so it can notice a `q` keypress while waiting, and making it dequeue only
59//! the reply would delete that. The honest fix is a bounded stash, which is real work and is not
60//! done - so the limitation is recorded here (26.7) instead of being implied away.
61//!
62//! **It reaches further than this module.** Every `Fs::call` goes through
63//! [`request_within_notice`](request_within_notice), and so does every `Net` call, so a handle built
64//! with `Fs::with_notice` or `Net::with_notice` is on that path too. Use the plain constructors in a
65//! service that serves; `with_notice` is for a foreground command where a person is waiting and may
66//! want to press `q`.
67//!
68//! # Authority
69//!
70//! Every call takes `&ServiceContext`. There is no global and no ambient handle. When this task holds
71//! no send capability for `peer`, [`request_within`] asks the kernel's name directory for one
72//! (`AcquireSendCap`), exactly as it does for a stale one - and the kernel grants that only to a task
73//! whose spawn request declared `peer` as a send peer, or that holds `ACQUIRE_ANY` (the shell, the
74//! probes). Anyone else gets `Unreachable`. So this library widens nothing, but it is not true that
75//! it never acquires: a task holding `ACQUIRE_ANY` can reach any named service through it.
76
77use godspeed_sdk::ipc::{IpcError, Message};
78use godspeed_sdk::service_context::{DeadlineOutcome, ReqOutcome, ServiceContext};
79
80use crate::error::Error;
81
82/// How long to wait before calling an answer late, when the caller does not say.
83///
84/// Five seconds is the figure `services/copier` uses for a single filesystem chunk, chosen because
85/// it is comfortably longer than any healthy answer and short enough that a wedged peer does not
86/// hold a utility forever. An operation that legitimately takes longer (a whole-volume scrub) must
87/// say so with [`request_within`] rather than raising this for everyone.
88pub const DEFAULT_SECS: i64 = 5;
89
90// EVERY REQUEST HERE IS `#[inline]`, as the SDK calls under them are, and for their reason: a `Message`
91// is 4 KiB by value, so a request that is its own frame costs its caller a second copy of the reply on
92// the stack. The shell, whose stack is tight on pipe paths, calls these.
93
94/// Ask `peer` for something and wait up to [`DEFAULT_SECS`] for the answer.
95///
96/// See [`request_within`]; this is that, with the common deadline.
97#[inline]
98pub fn request(ctx: &ServiceContext, peer: &str, msg: &Message) -> Result<Message, Error> {
99    request_within(ctx, peer, msg, DEFAULT_SECS)
100}
101
102/// How long a wait must last before the caller is told it is lasting.
103///
104/// Two seconds is the shell's own figure. Short enough that a person does not decide the machine is
105/// dead; long enough that an ordinary answer never triggers it.
106pub const NOTICE_AFTER_SECS: i64 = 2;
107
108/// A bounded request with a callback fired once if the wait drags on - and, with a callback, NOT the
109/// same machinery as [`request_within`].
110///
111/// With `notice` set to `None` this IS [`request_within`]. With a callback it rides the SDK's `_qhint`
112/// request, which reads its outcomes differently, so:
113///
114/// - **A request that never left is retried once, as in [`request_within`]**, after reacquiring the
115///   peer by name; if it still cannot be sent the answer is [`Error::Unreachable`]. What differs is how
116///   the failed send is recognised: `_qhint` reports a failed send and a passed deadline as the same
117///   `Timeout`, but it reports the deadline only once it has passed, so a `Timeout` sooner than `secs`
118///   is the send that never left (as [`request_watched`] reads it). A deadline that passed is
119///   [`Error::OutcomeUnknown`] and is never re-sent. (Until 2026-10-16 there was no retry at all, and a
120///   peer that started after the caller could not be reached until something else reacquired it.)
121/// - **A peer that died holding the request is [`Error::OutcomeUnknown`]**, never
122///   [`Error::PeerDied`]: the wait is a timed receive, so no `ReplyDead` reaches it.
123///
124/// It also adds [`Error::Cancelled`], for a person who pressed `q` - and it is not safe for a task
125/// that serves clients; see the module header.
126///
127/// # Why this exists, given the rest of this module is about not being clever
128///
129/// `services/shell` prints `[q] quit` when a network request lingers, so a person waiting on an
130/// unreachable host can stop it rather than wonder. That is an interactive affordance and it is
131/// exactly the sort of thing a standard library should NOT own - which is why the callback takes no
132/// arguments and returns nothing. It means only **"you have been waiting a while"**. What to do
133/// about it - print, poll a key, set a flag - stays entirely with the caller, and this module keeps
134/// knowing nothing about consoles.
135///
136/// Without it the shell cannot move onto this library without losing that affordance, which would
137/// have been a real regression dressed up as a migration.
138#[inline]
139///
140/// `keep`, when given, receives every key the wait reads that is not a quit key, in order. A wait
141/// that watches for `q` has to read the console, and without it whatever the operator typed ahead
142/// is lost (`backlog/fixed/80` H16).
143pub fn request_within_notice(
144    ctx: &ServiceContext, peer: &str, msg: &Message, secs: i64, notice: Option<&dyn Fn()>,
145    keep: Option<&dyn Fn(u8)>,
146) -> Result<Message, Error> {
147    let notice = match notice {
148        None => return request_within(ctx, peer, msg, secs),
149        Some(f) => f,
150    };
151    let secs = secs.max(1);
152    for attempt in 0..2 {
153        let t0 = ctx.epoch_secs_monotonic();
154        let out = match keep {
155            Some(k) => ctx.request_with_reply_qhint_keeping(peer, msg, NOTICE_AFTER_SECS, secs, || notice(), k),
156            None => ctx.request_with_reply_qhint(peer, msg, NOTICE_AFTER_SECS, secs, || notice()),
157        };
158        // Name the outcome before acting on it, as `request_watched` does. `ReqOutcome` answers a passed
159        // deadline and a send that never left with the same `Timeout`, but it reports the deadline only once
160        // it has passed - so a `Timeout` sooner than `secs` is the send that never left, and nothing else.
161        let outcome = match out {
162            ReqOutcome::Reply(r) => DeadlineOutcome::Reply(r),
163            // The caller stopped waiting. Reported as ITSELF, not folded into the timeout: a person
164            // pressing `q` and a service going silent are different facts and a caller will say
165            // different things about them. Collapsing those two would be the same mistake this module
166            // exists to stop the SDK's `Option` making.
167            ReqOutcome::Aborted => return Err(Error::Cancelled),
168            ReqOutcome::Timeout if ctx.epoch_secs_monotonic().saturating_sub(t0) >= secs => DeadlineOutcome::Timeout,
169            ReqOutcome::Timeout => DeadlineOutcome::SendFailed,
170        };
171        match outcome {
172            DeadlineOutcome::Reply(r) => return Ok(r),
173            // A live peer that is slow: the request may have happened, so it is never re-sent.
174            DeadlineOutcome::Timeout => return Err(Error::OutcomeUnknown),
175            // THE SEND NEVER LEFT: no live capability to the peer - it restarted, or it started after the
176            // caller and was never acquired. Nothing happened, so the one safe retry: reacquire by name and
177            // send again. Until 2026-10-16 this answered `OutcomeUnknown` at once, and on the Wyse, where the
178            // shell starts before `net-stack`, the first `tcp` of a boot said "net-stack did not answer
179            // within 20s" after a few seconds, having never reached it.
180            DeadlineOutcome::SendFailed => {
181                if attempt == 0 && ctx.reacquire_cap(peer).is_ok() {
182                    continue;
183                }
184                return Err(Error::Unreachable);
185            }
186            DeadlineOutcome::QueueFull => return Err(Error::Busy),
187        }
188    }
189    Err(Error::Unreachable)
190}
191
192/// A COMMAND's watch over its waits, lent by the program the operator is typing at (the shell).
193///
194/// **Why the watch belongs to the caller and not to the wait** (`audits/2026-10-13-quit-audit.md` 7a). A
195/// person waits on a COMMAND, not on one request: a `copy` of a large file is hundreds of requests that each
196/// answer in milliseconds, and a notice timed per request never fires for it, however long the copy runs. So
197/// the clock, the `[q] quit` words and the record of whether the operator has quit live in the shell, once per
198/// command, and every wait the command makes reads them. A program that is not at a prompt lends no watch and
199/// gets the plain bounded wait.
200///
201/// The library still knows nothing about consoles: these are questions, and the answers are the caller's.
202pub trait Watch {
203    /// A command is running. When it is not (between commands, before the first prompt), a wait is plain:
204    /// nothing is shown and no key is read.
205    fn active(&self) -> bool;
206    /// The operator has asked to stop. Asked before every request, so a command made of many fast
207    /// requests stops at the next one - nothing further is sent.
208    fn stopped(&self) -> bool;
209    /// Seconds until the command's notice is due: 0 once it is due or has been shown.
210    fn notice_due_in(&self) -> i64;
211    /// The notice is due. The watch shows it once per command; a wait may call this more than once.
212    fn notice(&self);
213    /// A key a wait read that is not a quit key, kept for the command line that follows.
214    fn keep(&self, key: u8);
215    /// A wait was ended by the operator's quit key.
216    fn quit(&self);
217}
218
219/// A bounded request under a command's [`Watch`]: [`request_within`] when the watch is not active, and
220/// otherwise a request that shows the command's `[q] quit` and honours it.
221///
222/// - **[`Error::Cancelled`]** - the operator quit. If the watch had already stopped, NOTHING was sent; if the
223///   quit came during the wait, the request was delivered and **it may have happened** - a caller reporting a
224///   mutation says so.
225/// - The rest are [`request_within`]'s, including its one safe retry: the quittable SDK wait answers a passed
226///   deadline and a send that never left with the same `Timeout`, but it can only report the deadline once the
227///   deadline has passed, so a `Timeout` sooner than `secs` is a send that never left - nothing happened -
228///   and is retried once after reacquiring the peer.
229/// - Like every noticing wait, it takes the next reply rather than a reply matched to this request, so it is
230///   for a caller that tags its requests (`gs::fs`, `gs::net` do) and does not serve clients of its own.
231pub fn request_watched(
232    ctx: &ServiceContext, peer: &str, msg: &Message, secs: i64, watch: &dyn Watch,
233) -> Result<Message, Error> {
234    request_watched_sifted(ctx, peer, msg, secs, watch, &mut |_| true)
235}
236
237/// [`request_watched`] that takes only the reply `mine` accepts, and drops any other while it waits. When the
238/// watch is not active it is [`request_within`], which does not sift.
239///
240/// **Why a quittable wait needs this.** Quitting abandons a request whose reply is still on its way. The
241/// next request's wait would take that late reply as its own, and the next command fails for nothing -
242/// found by `osdev test quit` on a slow `fs`, where every command after a `q` failed ("storage
243/// unavailable", "see fs's log"). A caller that tags its requests passes a test for its own tag, so a late
244/// answer to a question already given up on is recognised and dropped.
245pub fn request_watched_sifted(
246    ctx: &ServiceContext, peer: &str, msg: &Message, secs: i64, watch: &dyn Watch,
247    mine: &mut dyn FnMut(&Message) -> bool,
248) -> Result<Message, Error> {
249    if !watch.active() {
250        return request_within(ctx, peer, msg, secs);
251    }
252    let secs = secs.max(1);
253    for attempt in 0..2 {
254        if watch.stopped() {
255            return Err(Error::Cancelled);
256        }
257        let due = watch.notice_due_in().max(0);
258        if due == 0 {
259            watch.notice();
260        }
261        let t0 = ctx.epoch_secs_monotonic();
262        let out = ctx.request_with_reply_keyhint_sifted_keeping(
263            peer, msg, due, secs, ServiceContext::QUIT_KEYS, || watch.notice(), |m| mine(m), &|k| watch.keep(k));
264        // Name the outcome before acting on it. The quittable SDK wait answers a passed deadline and a send
265        // that never left with the same `Timeout`, but it reports the deadline only once it has passed - so a
266        // `Timeout` sooner than `secs` is a send that never left, and nothing else.
267        let outcome = match out {
268            ReqOutcome::Reply(r) => DeadlineOutcome::Reply(r),
269            ReqOutcome::Aborted => {
270                watch.quit();
271                return Err(Error::Cancelled);
272            }
273            ReqOutcome::Timeout if ctx.epoch_secs_monotonic().saturating_sub(t0) >= secs => DeadlineOutcome::Timeout,
274            ReqOutcome::Timeout => DeadlineOutcome::SendFailed,
275        };
276        match outcome {
277            DeadlineOutcome::Reply(r) => return Ok(r),
278            // A live peer that is slow: never re-sent (it may have happened).
279            DeadlineOutcome::Timeout => return Err(Error::OutcomeUnknown),
280            // The send never left: the one safe retry, as `request_within`.
281            DeadlineOutcome::SendFailed => {
282                if attempt == 0 && ctx.reacquire_cap(peer).is_ok() {
283                    continue;
284                }
285                return Err(Error::Unreachable);
286            }
287            DeadlineOutcome::QueueFull => return Err(Error::Busy),
288        }
289    }
290    Err(Error::Unreachable)
291}
292
293/// [`request_within`] with NO retry: one send, one bounded wait, and the reason it failed.
294///
295/// For a caller that owns its own recovery - one that reacquires and says so in its log, or that does
296/// pace and retry a full queue because it knows its peer drains fast. The four failures are the ones
297/// [`request_within`] tells apart, and nothing is done about any of them:
298///
299/// - [`Error::Unreachable`] - the request never left (a stale capability, a peer mid-restart). Nothing
300///   happened; reacquire the name ([`crate::cap::reacquire`]) and ask again if you want to.
301/// - [`Error::Busy`] - the peer's queue was full. Nothing happened.
302/// - [`Error::OutcomeUnknown`] - no reply in time. **It may have happened.**
303/// - [`Error::PeerDied`] - it arrived and the peer died before answering. **It may have happened**;
304///   reacquire before asking again, and ask again only if the operation is safe to repeat.
305///
306/// Prefer [`request_within`], which does the one safe retry for you. This exists so that a caller with
307/// a different, deliberate policy does not have to leave the library to keep it.
308#[inline]
309pub fn request_once(
310    ctx: &ServiceContext, peer: &str, msg: &Message, secs: i64,
311) -> Result<Message, Error> {
312    match ctx.request_with_reply_call_err(peer, msg, secs) {
313        Ok(Some(r))              => Ok(r),
314        Ok(None)                 => Err(Error::OutcomeUnknown),
315        Err(IpcError::ReplyDead) => Err(Error::PeerDied),
316        Err(IpcError::QueueFull) => Err(Error::Busy),
317        Err(_)                   => Err(Error::Unreachable),
318    }
319}
320
321/// Ask `peer` for something and wait up to `secs` for the answer.
322///
323/// **Blocks** until the reply arrives or the deadline passes. **Not cancellable**: the deadline is
324/// the only bound, which is why it is a parameter rather than a constant.
325///
326/// **Authority:** uses this task's send capability for `peer`. When it holds none, or a stale one, it
327/// asks the kernel's name directory for a fresh one (`AcquireSendCap`), which the kernel grants only
328/// to a task whose spawn request declared `peer` or that holds `ACQUIRE_ANY` - see the module header.
329///
330/// **If the peer restarts** while the request is in flight, the send fails rather than vanishing,
331/// and this reacquires the peer by name and sends once more - so an ordinary restart is invisible
332/// to the caller, which is what §14.3 asks of every client. If the peer dies AFTER receiving the
333/// request, the kernel wakes this call with `ReplyDead` and you get [`Error::PeerDied`], not
334/// re-sent, because the work may have been done.
335///
336/// # Errors
337///
338/// - [`Error::Unreachable`] - the request never left, twice, or the name could not be reacquired
339///   between the two. Nothing happened.
340/// - [`Error::Busy`] - the peer's queue is full. Nothing happened.
341/// - [`Error::OutcomeUnknown`] - no reply in time. **It may have happened.**
342/// - [`Error::PeerDied`] - the request reached the peer and it died before answering (`ReplyDead`).
343///   **It may have happened.** Not retried here; a caller whose operation is safe to repeat
344///   reacquires and asks again itself.
345#[inline]
346pub fn request_within(
347    ctx: &ServiceContext, peer: &str, msg: &Message, secs: i64,
348) -> Result<Message, Error> {
349    // `CallDeadline`, NOT a send followed by a plain recv.
350    //
351    // The kernel matches the reply to the one-shot reply capability this carries and leaves every
352    // other message queued. That is the difference between a library a SERVING task can call and one
353    // it cannot: a plain recv takes whatever lands next, so a client request that arrives mid-wait is
354    // consumed, misparsed, and lost. CLAUDE.md 8.2's amendment adds the primitive for exactly this,
355    // and `services/recorder` demonstrated the gap the day it stopped using it - a capture that died
356    // because the shell asked it a question at the wrong moment.
357    //
358    // There is nothing to hand back to the caller afterwards, either: the unrelated message was
359    // never taken, so it is still in the queue for the caller's own loop.
360    match ctx.request_with_reply_call_err(peer, msg, secs) {
361        Ok(Some(r)) => Ok(r),
362
363        // NEVER retried. See the module header.
364        Ok(None) => Err(Error::OutcomeUnknown),
365
366        // THE REQUEST ARRIVED AND THE REPLIER DIED. NEVER retried, for the same reason a timeout is
367        // not: `ReplyDead` is the kernel saying the request was delivered and its replier died holding
368        // the reply capability (CLAUDE.md 8.6), so the work may have been done. This used to fall into
369        // the arm below with every send failure and be re-sent - the one thing this module exists to
370        // refuse, done silently whenever a peer died mid-request. Found moving every service onto this
371        // library (2026-10-09).
372        Err(IpcError::ReplyDead) => Err(Error::PeerDied),
373
374        // NOT retried, deliberately. Congestion clears on its own and the caller knows its own
375        // pacing; a library that retries a full queue on your behalf turns one late request into
376        // two and calls it help.
377        Err(IpcError::QueueFull) => Err(Error::Busy),
378
379        // THE ONE RETRY THAT IS SAFE. The send did not happen, so re-sending is the same
380        // operation rather than a second one. `reacquire_cap` is what makes the retry worth
381        // doing: without it the second send uses the same stale handle and fails identically.
382        Err(_) => {
383            if ctx.reacquire_cap(peer).is_err() {
384                return Err(Error::Unreachable);
385            }
386            match ctx.request_with_reply_call_err(peer, msg, secs) {
387                Ok(Some(r))              => Ok(r),
388                Ok(None)                 => Err(Error::OutcomeUnknown),
389                Err(IpcError::ReplyDead) => Err(Error::PeerDied),
390                Err(IpcError::QueueFull) => Err(Error::Busy),
391                Err(_)                   => Err(Error::Unreachable),
392            }
393        }
394    }
395}