Skip to main content

liminal_server/server/connection/link_route/
executor.rs

1//! Far requests use full supervised connections and the existing grant fences.
2
3use crate::link::LinkHost;
4use crate::link::calls::{CallError, CallExecution, CallExecutor, CallReply, CallRequest};
5use crate::server::connection::{
6    ConnectionHandle, ConnectionSupervisor, LoopbackClientEnd, LoopbackDuplex,
7};
8use liminal::protocol::{Frame, FrameHeader, codec};
9use std::io::{Read, Write};
10use std::num::NonZeroUsize;
11use std::sync::atomic::{AtomicUsize, Ordering};
12use std::sync::{Arc, Condvar, Mutex};
13use std::thread::JoinHandle;
14use zeroize::Zeroizing;
15
16struct Executor {
17    supervisor: ConnectionSupervisor,
18    ring_bytes: NonZeroUsize,
19    answer_bytes: NonZeroUsize,
20    retained: Arc<Retirement>,
21}
22#[derive(Default)]
23struct Retirement {
24    count: AtomicUsize,
25    signal: Mutex<()>,
26    changed: Condvar,
27}
28impl std::fmt::Debug for Executor {
29    fn fmt(&self, out: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30        out.debug_struct("LinkCallExecutor")
31            .field("ring_bytes", &self.ring_bytes)
32            .field("answer_bytes", &self.answer_bytes)
33            .finish_non_exhaustive()
34    }
35}
36
37impl ConnectionSupervisor {
38    /// Installs far calls after this supervisor's normal Lys admission is ready.
39    /// `ring_bytes` bounds each inner transport; `answer_bytes` bounds a far reply
40    /// before allocation, independently of the outer Noise frame ceiling.
41    ///
42    /// # Errors
43    /// A second executor was installed on the same host.
44    pub fn install_link_call_executor(
45        &self,
46        host: &LinkHost,
47        ring_bytes: NonZeroUsize,
48        answer_bytes: NonZeroUsize,
49    ) -> Result<Arc<dyn CallExecutor>, CallError> {
50        let executor: Arc<dyn CallExecutor> = Arc::new(Executor {
51            supervisor: self.clone(),
52            ring_bytes,
53            answer_bytes,
54            retained: Arc::new(Retirement::default()),
55        });
56        host.install_call_executor(&executor)?;
57        Ok(executor)
58    }
59}
60
61impl CallExecutor for Executor {
62    fn retained(&self) -> usize {
63        self.retained.count.load(Ordering::Acquire)
64    }
65    fn observe_empty(&self) -> Result<(), CallError> {
66        let signal = self
67            .retained
68            .signal
69            .lock()
70            .map_err(|_| CallError::Unavailable)?;
71        let signal = self
72            .retained
73            .changed
74            .wait_while(signal, |()| {
75                self.retained.count.load(Ordering::Acquire) != 0
76            })
77            .map_err(|_| CallError::Unavailable)?;
78        drop(signal);
79        Ok(())
80    }
81    fn start(
82        &self,
83        request: CallRequest,
84        completed: Box<dyn FnOnce(Result<CallReply, CallError>) + Send>,
85    ) -> Result<Box<dyn CallExecution>, CallError> {
86        let correlation = request.correlation()?;
87        let signal = self
88            .retained
89            .signal
90            .lock()
91            .map_err(|_| CallError::Unavailable)?;
92        let (mut client, server) = LoopbackDuplex::bounded(self.ring_bytes.get());
93        let handle = self
94            .supervisor
95            .spawn_loopback_connection(server)
96            .map_err(|_| CallError::Capacity)?;
97        let id = request.id();
98        let bound = self.answer_bytes.get();
99        let dispatcher = tracing::dispatcher::get_default(Clone::clone);
100        let worker = std::thread::Builder::new()
101            .name("liminal-linked-call".into())
102            .spawn(move || {
103                tracing::dispatcher::with_default(&dispatcher, || {
104                    let answer = exchange(&mut client, &request, correlation, bound)
105                        .and_then(|frame| CallReply::new(id, frame));
106                    drop(request);
107                    drop(client);
108                    completed(answer);
109                });
110            })
111            .map_err(|_| CallError::Unavailable)?;
112        self.retained.count.fetch_add(1, Ordering::AcqRel);
113        drop(signal);
114        Ok(Box::new(Execution {
115            handle,
116            worker: Some(worker),
117            retained: Arc::clone(&self.retained),
118        }))
119    }
120}
121
122struct Execution {
123    handle: ConnectionHandle,
124    worker: Option<JoinHandle<()>>,
125    retained: Arc<Retirement>,
126}
127impl std::fmt::Debug for Execution {
128    fn fmt(&self, out: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129        out.debug_struct("LinkCallExecution")
130            .field("connection", &self.handle.pid())
131            .finish_non_exhaustive()
132    }
133}
134impl CallExecution for Execution {
135    fn finish(&mut self) -> Result<(), CallError> {
136        // A live loopback connection is asked to exit. The scheduler refuses
137        // the ask only for a process that already holds its exit tombstone
138        // (still in the table while its exit propagates), so a refusal means
139        // the connection is exiting on its own and the worker is joined as it
140        // is after an accepted ask. Reading the table again decided nothing:
141        // the tombstone keeps the pid in it either way.
142        if self.handle.is_live() && self.handle.request_crash().is_err() {
143            tracing::debug!(
144                connection = self.handle.pid(),
145                "linked call connection is already exiting"
146            );
147        }
148        if let Some(worker) = self.worker.take() {
149            worker.join().map_err(|_| CallError::Unavailable)?;
150        }
151        Ok(())
152    }
153}
154impl Drop for Execution {
155    fn drop(&mut self) {
156        if let Err(error) = self.finish() {
157            tracing::error!(%error, "linked call execution retirement failed");
158        }
159        if let Ok(signal) = self.retained.signal.lock() {
160            self.retained.count.fetch_sub(1, Ordering::AcqRel);
161            self.retained.changed.notify_all();
162            drop(signal);
163        } else {
164            tracing::error!("link_call_retirement_signal_unavailable");
165        }
166    }
167}
168
169fn exchange(
170    client: &mut LoopbackClientEnd,
171    request: &CallRequest,
172    correlation: (u32, u64),
173    bound: usize,
174) -> Result<Frame, CallError> {
175    client
176        .write_all(request.wire_bytes())
177        .map_err(|_| CallError::Unavailable)?;
178    match read(client, bound)? {
179        Frame::ConnectAck { .. } => {}
180        Frame::ConnectError {
181            reason_code,
182            message,
183            ..
184        } => return Ok(refusal(correlation, reason_code, message)),
185        _ => return Err(CallError::Invalid),
186    }
187    let frame = read(client, bound)?;
188    match &frame {
189        Frame::ConversationMessage {
190            stream_id,
191            conversation_id,
192            ..
193        }
194        | Frame::ConversationError {
195            stream_id,
196            conversation_id,
197            ..
198        } if (*stream_id, *conversation_id) == correlation => Ok(frame),
199        _ => Err(CallError::Correlation),
200    }
201}
202
203fn read(client: &mut impl Read, bound: usize) -> Result<Frame, CallError> {
204    let mut header = [0; FrameHeader::WIRE_LEN];
205    client
206        .read_exact(&mut header)
207        .map_err(|_| CallError::PeerGone)?;
208    let length = usize::try_from(u32::from_be_bytes(
209        header[6..10].try_into().map_err(|_| CallError::Invalid)?,
210    ))
211    .map_err(|_| CallError::Bound)?;
212    let total = header
213        .len()
214        .checked_add(length)
215        .filter(|length| *length <= bound)
216        .ok_or(CallError::Bound)?;
217    let mut bytes = Zeroizing::new(vec![0; total]);
218    bytes[..header.len()].copy_from_slice(&header);
219    client
220        .read_exact(&mut bytes[header.len()..])
221        .map_err(|_| CallError::PeerGone)?;
222    let (frame, used) = codec::decode(&bytes).map_err(|_| CallError::Invalid)?;
223    if used != total {
224        return Err(CallError::Invalid);
225    }
226    Ok(frame)
227}
228
229const fn refusal(correlation: (u32, u64), reason_code: u16, message: Option<String>) -> Frame {
230    Frame::ConversationError {
231        flags: 0,
232        stream_id: correlation.0,
233        conversation_id: correlation.1,
234        reason_code,
235        message,
236    }
237}