godspeed/ipc.rs
1// SPDX-License-Identifier: Apache-2.0
2//! Messages between services: sending one, receiving one, and what the failures mean.
3//!
4//! # The model in four sentences
5//!
6//! A service owns one endpoint with a bounded queue (16 messages, CLAUDE.md 8.5). A send COPIES the
7//! bytes into the receiver's queue and returns; it does not wait for the receiver to look at them. So
8//! a successful [`send`] means **queued, not processed** - if you need to know it was acted on, the
9//! protocol has to say so, which is what [`crate::call`] is for. And a message can carry a
10//! capability, which MOVES: the sender no longer holds it afterwards.
11//!
12//! # Why every failure here is worth reading rather than retrying
13//!
14//! The three you will actually meet say different things and want different answers:
15//!
16//! - [`Error::Unreachable`](crate::Error) - the peer died, or its name does not resolve right now.
17//! **Nothing happened.** The name is stable even though the instance is not (CLAUDE.md invariant
18//! 11), so the answer is [`cap::reacquire`](crate::cap::reacquire) and then retry - not a sleep.
19//! - [`Error::Busy`](crate::Error) - the peer is alive and its queue is full. **Nothing happened.**
20//! Congestion is transient; pace and retry, and do not go looking for a peer that never went away.
21//! - [`Error::InvalidInput`](crate::Error) - the message exceeds one page. Nothing will make that
22//! work; send less.
23//!
24//! A fourth appears only when you hand a capability away with [`send_granting`]:
25//!
26//! - [`Error::PermissionDenied`](crate::Error) - the capability you tried to send does not carry
27//! [`GRANT`](crate::cap::GRANT). **Nothing moved, so it is still yours** and the slot is still
28//! yours to reclaim. This is the first of the three checks CLAUDE.md 8.5 asks of every transfer,
29//! and it is the one a compiler cannot make for you.
30//!
31//! # The deadlock rule, which the kernel will not save you from
32//!
33//! If A and B both send to each other, at least one direction MUST use [`try_send`]. A blocking send
34//! into a full queue waits, and two services waiting on each other wait forever. The kernel does not
35//! detect this and does not break it (CLAUDE.md 8.9) - the supervisor's starvation watchdog is a last
36//! resort, not a design.
37
38use godspeed_sdk::ipc::IpcError;
39use godspeed_sdk::service_context::ServiceContext;
40
41pub use godspeed_sdk::ipc::Message;
42
43use crate::cap::Cap;
44use crate::error::Error;
45
46/// The largest message body, in bytes: one page (CLAUDE.md 8.5).
47///
48/// Bigger than this is not a tuning problem, it is a protocol problem - stream it in pieces, the way
49/// [`crate::fs`] does, rather than looking for a way to raise the limit.
50pub const MAX_BYTES: usize = godspeed_sdk::ipc::MAX_PAYLOAD;
51
52// THE RECEIVES ARE `#[inline(always)]`, AND THAT IS LOAD-BEARING. A `Message` is 4 KiB by value, so a
53// receive that is its own frame costs every caller a second 4 KiB of stack - and the shell, whose user
54// stack is tight on pipe paths, has overflowed on less. The SDK's `recv_timeout` says the same.
55
56/// The kernel's exact refusal, for the few programs whose job is to tell refusals apart.
57///
58/// [`Error`] is deliberately coarse: a sender is told whether the message left, not which of the
59/// kernel's reasons stopped it, because almost every caller does the same thing about all of them
60/// (CLAUDE.md 7.7). Two kinds of program genuinely act on the difference, and this is for them:
61///
62/// - **a conformance test**, whose whole purpose is to assert that the kernel answered `EndpointDead`
63/// and not `CapNotHeld` (`services/probe`, the section 22 suites);
64/// - **a fault injector that counts deaths** (`chaos`), where a dead endpoint is a kill landing and a
65/// stale or revoked capability is not.
66///
67/// An ordinary service uses the functions below, at this module's top level. Reaching for these to log a more specific reason is
68/// the wrong trade: the coarse answer is the one with a defined obligation attached to it.
69pub mod exact {
70 use godspeed_sdk::ipc::Message;
71 use godspeed_sdk::service_context::ServiceContext;
72
73 use crate::cap::Cap;
74
75 pub use godspeed_sdk::capability::CapError;
76 pub use godspeed_sdk::ipc::IpcError;
77
78 /// [`super::send`], reporting the kernel's own refusal.
79 pub fn send(ctx: &ServiceContext, peer: &str, msg: &Message) -> Result<(), IpcError> {
80 ctx.send(peer, msg)
81 }
82
83 /// [`super::try_send`], reporting the kernel's own refusal.
84 pub fn try_send(ctx: &ServiceContext, peer: &str, msg: &Message) -> Result<(), IpcError> {
85 ctx.try_send(peer, msg)
86 }
87
88 /// [`super::send_to`], reporting the kernel's own refusal.
89 pub fn send_to(ctx: &ServiceContext, cap: Cap, msg: &Message) -> Result<(), IpcError> {
90 ctx.send_by_handle(cap.handle(), msg)
91 }
92
93 /// [`super::try_send_to`], reporting the kernel's own refusal.
94 pub fn try_send_to(ctx: &ServiceContext, cap: Cap, msg: &Message) -> Result<(), IpcError> {
95 ctx.try_send_by_handle(cap.handle(), msg)
96 }
97
98 /// [`super::send_granting`], reporting the kernel's own refusal.
99 pub fn send_granting(ctx: &ServiceContext, to: Cap, granting: Cap, msg: &Message) -> Result<(), IpcError> {
100 ctx.send_with_cap_by_handle(to.handle(), granting.handle(), msg)
101 }
102}
103
104/// Translate a transport failure into the one error type a program handles.
105///
106/// The mapping is about what the CALLER should do rather than about which syscall said no: to a
107/// sender, a dead endpoint and a revoked capability are one situation - the message never left -
108/// and a full queue is a different one.
109///
110/// **This deliberately differs from [`crate::resource`], and the difference is load-bearing.** An
111/// invocation there reports a dead or revoked capability as [`Error::Revoked`], whose
112/// [`retry_is_safe`](crate::Error::retry_is_safe) is `false`, because the capability itself must be
113/// re-opened before a retry means anything. A send reports [`Error::Unreachable`], whose
114/// `retry_is_safe` is `true`, because reacquiring the NAME and sending again is exactly the right
115/// move (CLAUDE.md invariant 11). Same kernel error, different obligation on the caller.
116fn from_ipc(e: IpcError) -> Error {
117 use godspeed_sdk::capability::CapError;
118 match e {
119 IpcError::QueueFull => Error::Busy,
120 IpcError::MessageTooLarge => Error::InvalidInput,
121 IpcError::CapError(CapError::CapInsufficientRights)
122 | IpcError::CapError(CapError::CapNotGrantable)
123 | IpcError::CapError(CapError::CapWrongScope) => Error::PermissionDenied,
124 _ => Error::Unreachable,
125 }
126}
127
128/// Send to a service by NAME, blocking if its queue is full.
129///
130/// Returns when the message is queued. That is not the same as handled - see the module header.
131///
132/// Blocking is the right default for a client talking to a server it does not also serve. If the
133/// peer can send to YOU, use [`try_send`] in at least one direction (CLAUDE.md 8.9).
134pub fn send(ctx: &ServiceContext, peer: &str, msg: &Message) -> Result<(), Error> {
135 ctx.send(peer, msg).map_err(from_ipc)
136}
137
138/// Send to a service by NAME, refusing rather than waiting if its queue is full.
139///
140/// [`Error::Busy`](crate::Error) means the message was NOT queued and nothing happened. This is the
141/// direction that breaks a mutual-send deadlock, and the one a server uses to answer a client.
142pub fn try_send(ctx: &ServiceContext, peer: &str, msg: &Message) -> Result<(), Error> {
143 ctx.try_send(peer, msg).map_err(from_ipc)
144}
145
146/// Send through a capability you already hold, rather than resolving a name.
147///
148/// Faster and more honest than a name lookup when you have the capability: authority is the thing you
149/// hold, and this says so at the call site.
150pub fn send_to(ctx: &ServiceContext, cap: Cap, msg: &Message) -> Result<(), Error> {
151 ctx.send_by_handle(cap.handle(), msg).map_err(from_ipc)
152}
153
154/// Non-blocking [`send_to`].
155pub fn try_send_to(ctx: &ServiceContext, cap: Cap, msg: &Message) -> Result<(), Error> {
156 ctx.try_send_by_handle(cap.handle(), msg).map_err(from_ipc)
157}
158
159/// Send a message that CARRIES a capability, giving it away.
160///
161/// The capability MOVES (CLAUDE.md 8.5). Three things are true every time and none of them are
162/// checked for you at compile time:
163///
164/// 1. The capability must hold [`GRANT`](crate::cap::GRANT), or this is refused.
165/// 2. On success it is **no longer yours**. Using it afterwards is using something you do not hold.
166/// 3. On failure it **stayed** - reclaim the slot with [`cap::remove`](crate::cap::remove) rather
167/// than leaking it.
168///
169/// So the result is never ignorable: it decides what you may do next.
170pub fn send_granting(
171 ctx: &ServiceContext,
172 to: Cap,
173 granting: Cap,
174 msg: &Message,
175) -> Result<(), Error> {
176 ctx.send_with_cap_by_handle(to.handle(), granting.handle(), msg)
177 .map_err(from_ipc)
178}
179
180/// Block until a message arrives on this service's endpoint.
181///
182/// This takes whatever is next. If this service also awaits REPLIES on the same endpoint - which it
183/// does, because a task owns exactly one - a plain receive can swallow an unrelated client's request
184/// and drop it. That is not hypothetical; it cost this project a day (CLAUDE.md 8.2). When you are
185/// waiting for an answer to something you asked, use [`crate::call`], which matches the reply to its
186/// own reply capability and leaves everything else queued.
187#[inline(always)]
188pub fn recv(ctx: &ServiceContext) -> Message {
189 ctx.recv()
190}
191
192/// Take a message if one is already waiting, without blocking.
193///
194/// `None` means the queue was empty at that instant and nothing more.
195#[inline(always)]
196pub fn try_recv(ctx: &ServiceContext) -> Option<Message> {
197 ctx.try_recv()
198}
199
200/// Block for a message, giving up after `secs`.
201///
202/// `None` is the deadline passing - a fact about time, not a failure of the peer. A failed receive on
203/// this task's own endpoint is not folded into it: the task dies loudly, as a plain [`recv`] does,
204/// and the supervisor restarts it (`backlog/fixed/80` S5). Prefer this to
205/// a bare [`recv`] anywhere a missing message would otherwise hang the service forever, which is
206/// every place a peer can die (CLAUDE.md 26.6).
207#[inline(always)]
208pub fn recv_within(ctx: &ServiceContext, secs: i64) -> Option<Message> {
209 ctx.recv_timeout(ctx.duration_cycles((secs.max(0) as u64) * 1000))
210}
211
212/// [`recv_within`], bounded in MILLISECONDS.
213///
214/// For a driver or a poller whose rhythm is shorter than a second. Same contract: `None` is the
215/// deadline passing, and the wait takes whatever arrives next - see [`recv`] on why that matters to a
216/// task that also awaits replies.
217#[inline(always)]
218pub fn recv_within_ms(ctx: &ServiceContext, ms: u64) -> Option<Message> {
219 ctx.recv_timeout(ctx.duration_cycles(ms))
220}
221
222/// Answer a request on the reply capability its client sent, then give that capability back.
223///
224/// Both halves, every time, which is why this is one call. A reply capability is ONE-SHOT and occupies
225/// a slot in this task's table until it is removed: answered and kept, it leaks a slot per request
226/// until the table is full and the service can no longer receive one (CLAUDE.md 8.5). Non-blocking,
227/// because a client that stopped waiting must never stall the server that answers it; the result says
228/// whether the answer was queued, and either way the capability is gone afterwards.
229pub fn reply(ctx: &ServiceContext, reply_cap: Cap, msg: &Message) -> Result<(), Error> {
230 let sent = try_send_to(ctx, reply_cap, msg);
231 crate::cap::remove(ctx, reply_cap);
232 sent
233}
234
235/// The capability that arrived embedded in the message just received, if any.
236///
237/// Call it directly after the receive that carried it: it reports the LAST receive, so anything in
238/// between takes its place. A request/reply server calls this to get the reply capability its client
239/// sent (CLAUDE.md 8.2).
240pub fn take_sent_cap(ctx: &ServiceContext) -> Option<Cap> {
241 ctx.take_pending_cap().map(Cap)
242}
243
244/// Sleep forever, having nothing left to do.
245///
246/// For a service whose work is finished but whose death would be noise - it holds its slot and its
247/// capabilities and consumes no CPU. A service that should be gone should exit instead.
248pub fn park(ctx: &ServiceContext) -> ! {
249 ctx.park()
250}
251
252/// A capability to a peer this service was WIRED TO at spawn, by position.
253///
254/// Not a name lookup. These are the send peers this service's SPAWN REQUEST named - the supervisor's
255/// spawn row, not the `.toml` contract, which the kernel never reads (CLAUDE.md 13.6) - installed
256/// before this service ran, and position `0` is the first of them. A pipe stage
257/// reaches the next stage this way, which is what lets it send downstream while holding authority to
258/// reach nothing else (CLAUDE.md appendix D.3).
259///
260/// `None` means no peer was wired at that position - the spawn request named none there, or the
261/// composition that would have supplied it did not happen.
262pub fn peer_at(ctx: &ServiceContext, idx: usize) -> Option<Cap> {
263 ctx.send_peer_at(idx).map(Cap::from_handle)
264}
265
266/// A capability to a peer this service was wired to at spawn, by name.
267///
268/// The same set as [`peer_at`], addressed by the peer's name rather than by position. Use this when
269/// the spawn request names several peers and position would be a guess.
270pub fn peer(ctx: &ServiceContext, name: &str) -> Option<Cap> {
271 ctx.send_peer_handle(name).map(Cap::from_handle)
272}