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}