Skip to main content

microsandbox_protocol_client/
client.rs

1//! Shared framed connection; protocol setup and application behavior stay outside.
2
3use std::path::Path;
4use std::sync::Arc;
5
6use microsandbox_protocol::codec;
7use tokio::sync::{mpsc, oneshot};
8use tokio::task::JoinHandle;
9use tokio::time::Instant;
10use zeroize::{Zeroize, Zeroizing};
11
12use crate::router::{self, Lease, State, WriteCommand};
13use crate::{
14    ByteTransport, ClientError, ClientResult, ConnectOptions, Connector, Delivery, ErrorKind,
15    Established, IntoOutboundMessage, LocalConnector, Message, Protocol, RawFrame, RawStream,
16    Request, RequestOptions, Stream,
17};
18
19//--------------------------------------------------------------------------------------------------
20// Types
21//--------------------------------------------------------------------------------------------------
22
23/// Cheap shared handle to one reader, writer, allocator, and set of subscriptions.
24pub struct Client<P: Protocol> {
25    pub(crate) inner: Arc<Owner<P>>,
26}
27
28pub(crate) struct Owner<P: Protocol> {
29    pub(crate) state: Arc<State>,
30    pub(crate) ready: P::Ready,
31    pub(crate) codec: Arc<dyn crate::EnvelopeCodec>,
32    writer: mpsc::Sender<WriteCommand>,
33    handles: Vec<JoinHandle<()>>,
34}
35
36enum Packet {
37    Frame(RawFrame),
38    Exact(Zeroizing<Vec<u8>>),
39}
40
41//--------------------------------------------------------------------------------------------------
42// Methods
43//--------------------------------------------------------------------------------------------------
44
45impl<P: Protocol> Client<P> {
46    /// Connect a native endpoint with a single total setup deadline.
47    pub async fn connect(path: impl AsRef<Path>) -> ClientResult<Self> {
48        Self::connect_with(path, |options| options).await
49    }
50
51    /// Configure a native connection without modifying its endpoint name.
52    pub async fn connect_with(
53        path: impl AsRef<Path>,
54        configure: impl FnOnce(ConnectOptions) -> ConnectOptions,
55    ) -> ClientResult<Self> {
56        Self::connect_connector_with(&LocalConnector::new(path), configure).await
57    }
58
59    /// Connect through a supplied repeatable dialer.
60    pub async fn connect_connector(connector: &dyn Connector) -> ClientResult<Self> {
61        Self::connect_connector_with(connector, |options| options).await
62    }
63
64    /// Configure one deadline covering both dial and protocol establishment.
65    pub async fn connect_connector_with(
66        connector: &dyn Connector,
67        configure: impl FnOnce(ConnectOptions) -> ConnectOptions,
68    ) -> ClientResult<Self> {
69        let options = configure(ConnectOptions::default());
70        options.limits.validate()?;
71        let deadline = crate::options::checked_deadline(options.setup_timeout)?;
72        if deadline <= Instant::now() {
73            return Err(ClientError::new(ErrorKind::Timeout));
74        }
75        tokio::time::timeout_at(deadline, async {
76            let stream = connector.connect(deadline).await?;
77            let established = P::establish(stream, options).await?;
78            Self::from_established(established).await
79        })
80        .await
81        .map_err(|_| ClientError::new(ErrorKind::Timeout))?
82    }
83
84    /// Establish the protocol on an exclusively owned caller transport.
85    pub async fn connect_stream(stream: impl ByteTransport) -> ClientResult<Self> {
86        Self::connect_stream_with(stream, |options| options).await
87    }
88
89    /// Configure setup for an already-dialed owned transport.
90    pub async fn connect_stream_with(
91        stream: impl ByteTransport,
92        configure: impl FnOnce(ConnectOptions) -> ConnectOptions,
93    ) -> ClientResult<Self> {
94        let options = configure(ConnectOptions::default());
95        options.limits.validate()?;
96        let deadline = crate::options::checked_deadline(options.setup_timeout)?;
97        if deadline <= Instant::now() {
98            return Err(ClientError::new(ErrorKind::Timeout));
99        }
100        tokio::time::timeout_at(deadline, async {
101            let established = P::establish(Box::new(stream), options).await?;
102            Self::from_established(established).await
103        })
104        .await
105        .map_err(|_| ClientError::new(ErrorKind::Timeout))?
106    }
107
108    /// Start routing after external protocol setup, validating ranges and limits.
109    pub async fn from_established(established: Established<P::Ready>) -> ClientResult<Self> {
110        established.ids.validate()?;
111        established.limits.validate()?;
112        let state = State::new(established.ids, established.limits, P::REUSE_IDS);
113        let (reader, writer) = tokio::io::split(established.transport);
114        let (sender, queue) = mpsc::channel(state.limits.queued_writes);
115        // Tasks own State, never Owner. Thus the last client/stream owner really
116        // closes the transport instead of forming a task/connection cycle.
117        let handles = vec![
118            tokio::spawn(router::reader_loop(reader, Arc::clone(&state))),
119            tokio::spawn(router::writer_loop(writer, queue, Arc::clone(&state))),
120        ];
121        Ok(Self {
122            inner: Arc::new(Owner {
123                state,
124                ready: established.ready,
125                codec: established.codec,
126                writer: sender,
127                handles,
128            }),
129        })
130    }
131
132    /// Shared immutable ready/welcome metadata.
133    pub fn ready(&self) -> &P::Ready {
134        &self.inner.ready
135    }
136
137    /// Whether the shared connection has terminated.
138    pub fn is_closed(&self) -> bool {
139        self.inner.state.is_closed()
140    }
141
142    /// Wait for shared closure, including an idle peer disconnect. Cancellation
143    /// of this wait does not close the connection or affect other callers.
144    pub async fn closed(&self) {
145        self.inner.state.cancelled().await;
146    }
147
148    /// Close all shared handles and wake every waiter; never reconnect implicitly.
149    pub async fn close(&self) {
150        self.inner.state.close(ErrorKind::Closed);
151        for handle in &self.inner.handles {
152            handle.abort();
153        }
154    }
155
156    /// Return the first response and retain drain state if it is nonterminal.
157    pub async fn request<M: IntoOutboundMessage<P>>(&self, message: M) -> ClientResult<Message> {
158        self.request_with(message, |options| options).await
159    }
160
161    /// Configure a local unary wait without imposing application success rules.
162    pub async fn request_with<M: IntoOutboundMessage<P>>(
163        &self,
164        message: M,
165        configure: impl FnOnce(RequestOptions) -> RequestOptions,
166    ) -> ClientResult<Message> {
167        let outbound = message.into_outbound(self.ready(), self.inner.codec.as_ref())?;
168        let frame = self
169            .request_raw_with(outbound.flags, outbound.body, configure)
170            .await?;
171        self.inner
172            .codec
173            .decode(frame)
174            .map_err(|error| error.with_delivery(Delivery::Unknown))
175    }
176
177    /// Execute a borrowed prepared request with optional checked result decoding.
178    pub async fn request_typed<R: Request<P>>(&self, request: &R) -> Result<R::Response, R::Error> {
179        self.request_typed_with(request, |options| options).await
180    }
181
182    /// Configure one checked unary attempt; unexpected streaming is an error.
183    pub async fn request_typed_with<R: Request<P>>(
184        &self,
185        request: &R,
186        configure: impl FnOnce(RequestOptions) -> RequestOptions,
187    ) -> Result<R::Response, R::Error> {
188        let message = self.request_with(request.message()?, configure).await?;
189        if message.flags & microsandbox_protocol::message::FLAG_TERMINAL == 0 {
190            return Err(ClientError::new(ErrorKind::InvalidData)
191                .with_delivery(Delivery::Unknown)
192                .into());
193        }
194        request.decode(message)
195    }
196
197    /// Open a message stream on an owned correlation ID.
198    pub async fn stream<M: IntoOutboundMessage<P>>(&self, message: M) -> ClientResult<Stream<P>> {
199        self.stream_with(message, |options| options).await
200    }
201
202    /// Configure the stream-opening wait; subsequent receives own their lifetime.
203    pub async fn stream_with<M: IntoOutboundMessage<P>>(
204        &self,
205        message: M,
206        configure: impl FnOnce(RequestOptions) -> RequestOptions,
207    ) -> ClientResult<Stream<P>> {
208        let outbound = message.into_outbound(self.ready(), self.inner.codec.as_ref())?;
209        Ok(Stream::from_raw(
210            self.stream_raw_with(outbound.flags, outbound.body, configure)
211                .await?,
212        ))
213    }
214
215    /// Return one raw frame without decoding or normalizing its envelope.
216    pub async fn request_raw(&self, flags: u8, body: Vec<u8>) -> ClientResult<RawFrame> {
217        self.request_raw_with(flags, body, |options| options).await
218    }
219
220    /// Set a deadline covering queue admission, write completion, and reply wait.
221    pub async fn request_raw_with(
222        &self,
223        flags: u8,
224        body: Vec<u8>,
225        configure: impl FnOnce(RequestOptions) -> RequestOptions,
226    ) -> ClientResult<RawFrame> {
227        let packet = self.packet(0, flags, body)?;
228        let mut stream = self.reserve_stream()?;
229        let lease = Arc::clone(&stream.receiver.lease);
230        let options = configure(RequestOptions::default());
231        self.attempt(&lease, options, async {
232            self.write_opening(&lease, packet).await?;
233            stream
234                .recv()
235                .await?
236                .ok_or_else(|| ClientError::new(ErrorKind::PeerClosed))
237        })
238        .await
239        // The owned receiver drops on every return/cancellation path. It keeps
240        // admitted, nonterminal IDs draining rather than immediately reusing them.
241    }
242
243    /// Open an opaque stream with the same ID lease as native message streams.
244    pub async fn stream_raw(&self, flags: u8, body: Vec<u8>) -> ClientResult<RawStream<P>> {
245        self.stream_raw_with(flags, body, |options| options).await
246    }
247
248    /// Configure opaque stream opening without inspecting its envelope.
249    pub async fn stream_raw_with(
250        &self,
251        flags: u8,
252        body: Vec<u8>,
253        configure: impl FnOnce(RequestOptions) -> RequestOptions,
254    ) -> ClientResult<RawStream<P>> {
255        let packet = self.packet(0, flags, body)?;
256        let stream = self.reserve_stream()?;
257        let lease = Arc::clone(&stream.receiver.lease);
258        let options = configure(RequestOptions::default());
259        self.attempt(&lease, options, self.write_opening(&lease, packet))
260            .await?;
261        Ok(stream)
262    }
263
264    /// Send a native or encoded payload on a currently live owned ID.
265    pub async fn send<M: IntoOutboundMessage<P>>(&self, id: u32, message: M) -> ClientResult<()> {
266        let lease = self.inner.state.owned(id)?;
267        self.send_owned(&lease, message).await
268    }
269
270    /// Send an opaque envelope on a currently live owned ID.
271    pub async fn send_raw(&self, id: u32, flags: u8, body: &[u8]) -> ClientResult<()> {
272        let lease = self.inner.state.owned(id)?;
273        self.send_raw_owned(&lease, flags, body).await
274    }
275
276    /// Serialize exact packet bytes without allocating an ID or subscription.
277    /// The caller owns packet semantics; byte limits and shared close still apply.
278    pub async fn write_unchecked(&self, packet: Vec<u8>) -> ClientResult<()> {
279        if packet.len() > self.inner.state.limits.buffered_bytes as usize {
280            return Err(ClientError::new(ErrorKind::Capacity));
281        }
282        self.write_packet(None, Packet::Exact(Zeroizing::new(packet)))
283            .await
284    }
285
286    pub(crate) async fn send_owned<M: IntoOutboundMessage<P>>(
287        &self,
288        lease: &Arc<Lease>,
289        message: M,
290    ) -> ClientResult<()> {
291        let outbound = message.into_outbound(self.ready(), self.inner.codec.as_ref())?;
292        self.write_packet(
293            Some(lease),
294            self.packet(lease.id, outbound.flags, outbound.body)?,
295        )
296        .await
297    }
298
299    pub(crate) async fn send_raw_owned(
300        &self,
301        lease: &Arc<Lease>,
302        flags: u8,
303        body: &[u8],
304    ) -> ClientResult<()> {
305        self.write_packet(Some(lease), self.packet(lease.id, flags, body.to_vec())?)
306            .await
307    }
308
309    fn reserve_stream(&self) -> ClientResult<RawStream<P>> {
310        let (lease, receiver) = self.inner.state.reserve()?;
311        Ok(RawStream::new(self.clone(), lease, receiver))
312    }
313
314    fn packet(&self, id: u32, flags: u8, body: Vec<u8>) -> ClientResult<Packet> {
315        if body.len() > self.inner.state.limits.max_frame_size as usize - 5 {
316            return Err(ClientError::new(ErrorKind::Capacity));
317        }
318        Ok(Packet::Frame(RawFrame { id, flags, body }))
319    }
320
321    async fn write_opening(&self, lease: &Arc<Lease>, mut packet: Packet) -> ClientResult<()> {
322        let Packet::Frame(frame) = &mut packet else {
323            unreachable!()
324        };
325        frame.id = lease.id;
326        self.write_packet(Some(lease), packet).await
327    }
328
329    async fn write_packet(&self, lease: Option<&Arc<Lease>>, packet: Packet) -> ClientResult<()> {
330        let bytes = Arc::clone(&self.inner.state.budget)
331            .acquire_many_owned(packet.len() as u32)
332            .await
333            .map_err(|_| self.inner.state.error())?;
334        let permit = self
335            .inner
336            .writer
337            .reserve()
338            .await
339            .map_err(|_| self.inner.state.error())?;
340        // Allocate the framed packet only after reserving queue bytes and an
341        // item slot. Waiting callers retain their input, not an extra packet
342        // copy outside the transport's buffer budget.
343        let packet = packet.encode()?;
344        let (ack, written) = oneshot::channel();
345        self.inner.state.admit(
346            lease,
347            permit,
348            WriteCommand {
349                packet,
350                ack,
351                _bytes: bytes,
352            },
353        )?;
354        written
355            .await
356            .map_err(|_| self.inner.state.error().with_delivery(Delivery::Unknown))?
357    }
358
359    async fn attempt<T>(
360        &self,
361        lease: &Lease,
362        options: RequestOptions,
363        future: impl std::future::Future<Output = ClientResult<T>>,
364    ) -> ClientResult<T> {
365        let result = match options
366            .request_timeout
367            .or(self.inner.state.limits.request_timeout)
368        {
369            Some(timeout) => {
370                let deadline = crate::options::checked_deadline(timeout)?;
371                // A zero/expired local wait must not poll a ready writer first:
372                // Tokio timeouts otherwise allow an immediately ready future
373                // to run even when the timer is already due.
374                if deadline <= Instant::now() {
375                    Err(ClientError::new(ErrorKind::Timeout))
376                } else {
377                    tokio::time::timeout_at(deadline, future)
378                        .await
379                        .unwrap_or_else(|_| Err(ClientError::new(ErrorKind::Timeout)))
380                }
381            }
382            None => future.await,
383        };
384        result.map_err(|error| error.with_delivery(lease.delivery()))
385    }
386}
387
388impl Packet {
389    fn len(&self) -> usize {
390        match self {
391            Self::Frame(frame) => frame.body.len() + 9,
392            Self::Exact(packet) => packet.len(),
393        }
394    }
395
396    fn encode(self) -> ClientResult<Zeroizing<Vec<u8>>> {
397        match self {
398            Self::Exact(packet) => Ok(packet),
399            Self::Frame(mut frame) => {
400                let mut packet = Zeroizing::new(Vec::with_capacity(frame.body.len() + 9));
401                let result = codec::encode_raw_to_buf(&frame, &mut packet);
402                frame.body.zeroize();
403                result.map_err(|_| ClientError::new(ErrorKind::InvalidData))?;
404                Ok(packet)
405            }
406        }
407    }
408}
409
410//--------------------------------------------------------------------------------------------------
411// Trait Implementations
412//--------------------------------------------------------------------------------------------------
413
414impl<P: Protocol> Clone for Client<P> {
415    fn clone(&self) -> Self {
416        Self {
417            inner: Arc::clone(&self.inner),
418        }
419    }
420}
421
422impl<P: Protocol> Drop for Owner<P> {
423    fn drop(&mut self) {
424        self.state.close(ErrorKind::Closed);
425        for handle in &self.handles {
426            handle.abort();
427        }
428    }
429}