weida 0.1.0-alpha.2

QUIC-native messaging framework: runtime, native QUIC transport, Req/Rep, Push/Pull, Pub/Sub, PAIR, SURVEY and BUS
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
//! Per-write chunk framing for the local stream transports.
//!
//! A QUIC stream ends two ways and says which: a FIN the reader sees as the
//! end of the payload, and a `RESET_STREAM` carrying a code the reader sees as
//! a failure. Neither local transport gets both from the kernel:
//!
//! * a **named pipe** has no half-close at all โ€” closing the handle would take
//!   the reply direction with it;
//! * an **`AF_UNIX` socket** has a half-close, and that is the whole problem:
//!   a shutdown and an abort are byte-for-byte the same thing at the reader,
//!   because Linux has no kernel-level abort for a stream socket (`SO_LINGER`
//!   with a zero timeout plus `close` is a plain close at the reader).
//!
//! So the distinction lives in the payload, for both of them, in one framing:
//!
//! ```text
//! 0x00 + u32 length + bytes    payload, as much as one write produced
//! 0x01                          FIN: the payload is complete
//! 0x02 + u64 code               RESET: the payload was abandoned, with why
//! ```
//!
//! Five bytes per write and nothing per byte, and it carries what the socket
//! could not: **a cancelled transfer is `Canceled` at the reader rather than a
//! short payload that looks complete**. That was the review finding B-245 โ€”
//! three documents asserted the guarantee for every transport and one
//! transport silently did not have it, with the failure mode an application
//! reading half a message as a whole one and nothing anywhere naming it.
//! [0012 ยง4.7(e)](../../../docs/decisions/0012-local-connection-grouping.md)
//! had already priced the alternative and this is the shape it chose, now
//! shared by both socket transports rather than implemented once per
//! transport.
//!
//! What the framing still cannot carry is the **reader's** refusal: there is
//! no `STOP_SENDING`, because on a pipe the writer's direction is the only one
//! the reader could signal on and the reply already owns it, and on a socket
//! closing the read half gives the writer an `EPIPE` with no code. A reader
//! that stops therefore **drains** โ€” the rest of the payload is read and
//! discarded in the background, so the writer finishes normally and learns of
//! the refusal from the reply.

use std::pin::Pin;
use std::task::{Context, Poll, ready};

use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, ReadBuf};
use weida_runtime::Exec;

/// A payload chunk: `u32` little-endian length, then the bytes.
const CHUNK_DATA: u8 = 0x00;
/// The end of the payload.
const CHUNK_FIN: u8 = 0x01;
/// The payload abandoned, with a `u64` little-endian code.
const CHUNK_RESET: u8 = 0x02;
/// The longest chunk header: kind plus a reset code.
const HEADER_MAX: usize = 9;
/// The header of a payload chunk: kind plus a `u32` length.
const HEADER_DATA: usize = 5;
/// Scratch for a draining reader.
const DRAIN_BUF: usize = 8 * 1024;

/// The peer abandoned its payload, with the code it gave.
///
/// Carried inside an `io::Error` so that a transport's `read_error` can
/// recover the code and report `Canceled` rather than a generic failure.
#[derive(Debug)]
pub(crate) struct PeerReset(pub(crate) u64);

impl std::fmt::Display for PeerReset {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "stream reset by the peer with code {}", self.0)
    }
}

impl std::error::Error for PeerReset {}

/// How a stream ended.
pub(crate) enum Marker {
    Fin,
    Reset(u64),
}

impl Marker {
    fn encode(&self) -> ([u8; HEADER_MAX], usize) {
        let mut bytes = [0u8; HEADER_MAX];
        match self {
            Marker::Fin => {
                bytes[0] = CHUNK_FIN;
                (bytes, 1)
            }
            Marker::Reset(code) => {
                bytes[0] = CHUNK_RESET;
                bytes[1..].copy_from_slice(&code.to_le_bytes());
                (bytes, HEADER_MAX)
            }
        }
    }
}

fn closed() -> std::io::Error {
    std::io::Error::new(std::io::ErrorKind::BrokenPipe, "stream already closed")
}

/// The writing half: frames every write as a chunk.
pub(crate) struct ChunkWriter<W> {
    io: Option<W>,
    exec: Exec,
    /// The chunk header being written, and how much of it is still owed.
    header: [u8; HEADER_MAX],
    header_len: usize,
    header_written: usize,
    /// Payload bytes the current chunk's header promised and that have not
    /// been written yet.
    body_left: usize,
    /// A FIN went out through `poll_shutdown`; `end` has nothing to add.
    finished: bool,
}

impl<W: AsyncWrite + Unpin + Send + 'static> ChunkWriter<W> {
    pub(crate) fn new(io: W, exec: Exec) -> ChunkWriter<W> {
        ChunkWriter {
            io: Some(io),
            exec,
            header: [0; HEADER_MAX],
            header_len: 0,
            header_written: 0,
            body_left: 0,
            finished: false,
        }
    }

    /// Writes the end-of-stream marker on the runtime, since the callers of
    /// `finish` and `reset` are synchronous.
    ///
    /// A chunk left half-written cannot be ended cleanly โ€” the reader is owed
    /// bytes this side no longer has โ€” so nothing is written and the reader
    /// sees the connection close when the last half goes.
    pub(crate) fn end(mut self, marker: Marker) {
        let Some(mut io) = self.io.take() else {
            return;
        };
        if self.finished || self.body_left > 0 || self.header_written != self.header_len {
            return;
        }
        let (bytes, len) = marker.encode();
        self.exec.spawn(async move {
            if let Err(e) = io.write_all(&bytes[..len]).await {
                tracing::debug!(error = %e, "local stream end marker not written");
                return;
            }
            let _ = io.flush().await;
        });
    }

    /// Writes what is owed of the current header; `Ready(Ok(()))` when none
    /// is.
    fn poll_header(&mut self, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
        let Some(io) = self.io.as_mut() else {
            return Poll::Ready(Err(closed()));
        };
        while self.header_written < self.header_len {
            let n = ready!(
                Pin::new(&mut *io)
                    .poll_write(cx, &self.header[self.header_written..self.header_len])
            )?;
            if n == 0 {
                return Poll::Ready(Err(std::io::Error::new(
                    std::io::ErrorKind::WriteZero,
                    "the local stream accepted no bytes",
                )));
            }
            self.header_written += n;
        }
        Poll::Ready(Ok(()))
    }

    fn poll_flush_inner(&mut self, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
        match self.io.as_mut() {
            Some(io) => Pin::new(io).poll_flush(cx),
            None => Poll::Ready(Ok(())),
        }
    }
}

impl<W: AsyncWrite + Unpin + Send + 'static> AsyncWrite for ChunkWriter<W> {
    fn poll_write(
        self: Pin<&mut Self>,
        cx: &mut Context<'_>,
        buf: &[u8],
    ) -> Poll<std::io::Result<usize>> {
        let this = self.get_mut();
        if this.io.is_none() || this.finished {
            return Poll::Ready(Err(closed()));
        }
        if buf.is_empty() {
            return Poll::Ready(Ok(0));
        }
        if this.body_left == 0 && this.header_written == this.header_len {
            // A new chunk: as long as this write, capped by the length field.
            let len = buf.len().min(u32::MAX as usize);
            this.header[0] = CHUNK_DATA;
            this.header[1..HEADER_DATA].copy_from_slice(&(len as u32).to_le_bytes());
            this.header_len = HEADER_DATA;
            this.header_written = 0;
            this.body_left = len;
        }
        ready!(this.poll_header(cx))?;
        let want = buf.len().min(this.body_left);
        let io = this.io.as_mut().expect("checked above");
        let n = ready!(Pin::new(io).poll_write(cx, &buf[..want]))?;
        this.body_left -= n;
        Poll::Ready(Ok(n))
    }

    fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
        self.get_mut().poll_flush_inner(cx)
    }

    /// A shutdown is a FIN, as it is on every other transport, written in
    /// place through the polls rather than on the runtime.
    fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
        let this = self.get_mut();
        if this.io.is_none() {
            return Poll::Ready(Ok(()));
        }
        if this.finished {
            return this.poll_flush_inner(cx);
        }
        if this.body_left > 0 {
            return Poll::Ready(Err(std::io::Error::other(
                "shutdown in the middle of a write",
            )));
        }
        if this.header_written == this.header_len {
            let (bytes, len) = Marker::Fin.encode();
            this.header = bytes;
            this.header_len = len;
            this.header_written = 0;
        }
        ready!(this.poll_header(cx))?;
        this.finished = true;
        this.poll_flush_inner(cx)
    }
}

enum ReadState {
    /// Reading a chunk header; the first byte says how long it is.
    Header {
        buf: [u8; HEADER_MAX],
        filled: usize,
    },
    /// Inside a payload chunk, with this many bytes still to come.
    Body { left: usize },
    /// FIN seen, RESET reported, or the connection failed: nothing more to
    /// read.
    Ended,
}

/// What a completed header means.
enum Chunk {
    Data(usize),
    Fin,
    Reset(u64),
}

/// The reading half: unframes chunks, ends at FIN, fails at RESET.
pub(crate) struct ChunkReader<R> {
    io: Option<R>,
    exec: Exec,
    state: ReadState,
}

impl<R: AsyncRead + Unpin + Send + 'static> ChunkReader<R> {
    pub(crate) fn new(io: R, exec: Exec) -> ChunkReader<R> {
        ChunkReader {
            io: Some(io),
            exec,
            state: ReadState::Header {
                buf: [0; HEADER_MAX],
                filled: 0,
            },
        }
    }

    /// Reads and discards the rest of the payload on the runtime, so the
    /// peer's writes complete instead of blocking on a stream nobody reads.
    pub(crate) fn drain(mut self) {
        if matches!(self.state, ReadState::Ended) || self.io.is_none() {
            return;
        }
        let exec = self.exec.clone();
        exec.spawn(async move {
            let mut scratch = vec![0u8; DRAIN_BUF];
            loop {
                match self.read(&mut scratch).await {
                    Ok(0) | Err(_) => return,
                    Ok(_) => {}
                }
            }
        });
    }

    /// How many header bytes a chunk of `kind` has, or `None` for a kind this
    /// side does not know.
    fn header_len(kind: u8) -> Option<usize> {
        match kind {
            CHUNK_DATA => Some(HEADER_DATA),
            CHUNK_FIN => Some(1),
            CHUNK_RESET => Some(HEADER_MAX),
            _ => None,
        }
    }

    fn poll_read_inner(
        &mut self,
        cx: &mut Context<'_>,
        buf: &mut ReadBuf<'_>,
    ) -> Poll<std::io::Result<()>> {
        let Some(io) = self.io.as_mut() else {
            return Poll::Ready(Ok(()));
        };
        loop {
            match &mut self.state {
                ReadState::Ended => return Poll::Ready(Ok(())),
                ReadState::Header {
                    buf: header,
                    filled,
                } => {
                    let need = if *filled == 0 {
                        1
                    } else {
                        match ChunkReader::<R>::header_len(header[0]) {
                            Some(need) => need,
                            None => {
                                return Poll::Ready(Err(std::io::Error::new(
                                    std::io::ErrorKind::InvalidData,
                                    format!("unknown local chunk kind {:#04x}", header[0]),
                                )));
                            }
                        }
                    };
                    if *filled < need {
                        let mut slice = ReadBuf::new(&mut header[*filled..need]);
                        ready!(Pin::new(&mut *io).poll_read(cx, &mut slice))?;
                        let n = slice.filled().len();
                        if n == 0 {
                            return Poll::Ready(Err(std::io::Error::new(
                                std::io::ErrorKind::UnexpectedEof,
                                "the local stream closed before the end of the payload",
                            )));
                        }
                        *filled += n;
                        continue;
                    }
                    let chunk = match header[0] {
                        CHUNK_DATA => {
                            let len =
                                u32::from_le_bytes([header[1], header[2], header[3], header[4]]);
                            Chunk::Data(len as usize)
                        }
                        CHUNK_FIN => Chunk::Fin,
                        _ => {
                            let mut code = [0u8; 8];
                            code.copy_from_slice(&header[1..HEADER_MAX]);
                            Chunk::Reset(u64::from_le_bytes(code))
                        }
                    };
                    match chunk {
                        Chunk::Data(0) => {
                            self.state = ReadState::Header {
                                buf: [0; HEADER_MAX],
                                filled: 0,
                            };
                        }
                        Chunk::Data(len) => self.state = ReadState::Body { left: len },
                        Chunk::Fin => {
                            self.state = ReadState::Ended;
                            return Poll::Ready(Ok(()));
                        }
                        Chunk::Reset(code) => {
                            self.state = ReadState::Ended;
                            return Poll::Ready(Err(std::io::Error::other(PeerReset(code))));
                        }
                    }
                }
                ReadState::Body { left } => {
                    let want = buf.remaining().min(*left);
                    if want == 0 {
                        return Poll::Ready(Ok(()));
                    }
                    // Initialised up front so that `advance` below is sound
                    // without an `unsafe` `assume_init`.
                    buf.initialize_unfilled_to(want);
                    let mut slice = buf.take(want);
                    ready!(Pin::new(&mut *io).poll_read(cx, &mut slice))?;
                    let n = slice.filled().len();
                    if n == 0 {
                        return Poll::Ready(Err(std::io::Error::new(
                            std::io::ErrorKind::UnexpectedEof,
                            "the local stream closed in the middle of a chunk",
                        )));
                    }
                    buf.advance(n);
                    *left -= n;
                    if *left == 0 {
                        self.state = ReadState::Header {
                            buf: [0; HEADER_MAX],
                            filled: 0,
                        };
                    }
                    return Poll::Ready(Ok(()));
                }
            }
        }
    }
}

impl<R: AsyncRead + Unpin + Send + 'static> AsyncRead for ChunkReader<R> {
    fn poll_read(
        self: Pin<&mut Self>,
        cx: &mut Context<'_>,
        buf: &mut ReadBuf<'_>,
    ) -> Poll<std::io::Result<()>> {
        let this = self.get_mut();
        let polled = this.poll_read_inner(cx, buf);
        if let Poll::Ready(Err(_)) = &polled {
            // Any failure ends the stream: a drain must not spin on it.
            this.state = ReadState::Ended;
        }
        polled
    }
}

/// A read error as both socket transports report it: a peer's RESET by its
/// code, anything else as a transport failure.
pub(crate) fn read_error(error: std::io::Error) -> weida_core::Error {
    match error
        .get_ref()
        .and_then(|inner| inner.downcast_ref::<PeerReset>())
    {
        Some(reset) => weida_protocol::codes::stop_reason(reset.0).into(),
        None => weida_core::Error::Transport(format!("local stream read failed: {error}")),
    }
}