Complete mmap transport: functional MmapSender/MmapReceiver + SyncPeer::Mmap
MmapSender and MmapReceiver were structural stubs with only open() methods.
Now each holds its own MmapMut view of the channel file and implements full
ring-buffer send/recv using AtomicU64 Acquire/Release stores for correct
cross-process visibility.
Extracted shared ring_send/ring_recv logic used by all three types
(MmapChannel, MmapSender, MmapReceiver). Replaced plain write_u64/read_u64
header accessors with atomic_store/atomic_load in the hot path; plain
helpers retained for one-time initialisation in create().
SyncPeer gains a Mmap { send_ch, receiver } variant; PipeWriteHalf and
PipeReadHalf gain Mmap arms. Async SyncPeer::send/recv/into_pipe_halves
wrap the blocking spin-wait calls with tokio::task::block_in_place.
Added 5 new mmap tests: sender_receiver_open_same_file, roundtrip,
multiple_messages, ring_wraps, cross_thread (two-thread SPSC).
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
1c107fe58a
commit
f1bdf921be
@@ -3,7 +3,7 @@
|
|||||||
//! - [`protocol`]: `SyncMessage` wire protocol + length-prefixed framing
|
//! - [`protocol`]: `SyncMessage` wire protocol + length-prefixed framing
|
||||||
//! - [`tcp`]: Async TCP transport (tokio)
|
//! - [`tcp`]: Async TCP transport (tokio)
|
||||||
//! - [`quic`]: QUIC transport (quinn 0.11, TLS 1.3)
|
//! - [`quic`]: QUIC transport (quinn 0.11, TLS 1.3)
|
||||||
//! - [`mmap`]: Memory-mapped local transport (Phase 4 stub)
|
//! - [`mmap`]: Memory-mapped local transport — zero-copy same-node IPC
|
||||||
//! - [`error`] — [`TransportError`]
|
//! - [`error`] — [`TransportError`]
|
||||||
|
|
||||||
#![deny(unsafe_code)]
|
#![deny(unsafe_code)]
|
||||||
@@ -16,7 +16,7 @@ pub mod quic;
|
|||||||
pub mod tcp;
|
pub mod tcp;
|
||||||
|
|
||||||
pub use error::TransportError;
|
pub use error::TransportError;
|
||||||
pub use mmap::{DEFAULT_CAPACITY, MmapChannel};
|
pub use mmap::{DEFAULT_CAPACITY, MmapChannel, MmapReceiver, MmapSender};
|
||||||
pub use peer::{PipeReadHalf, PipeWriteHalf, SyncPeer};
|
pub use peer::{PipeReadHalf, PipeWriteHalf, SyncPeer};
|
||||||
pub use protocol::SyncMessage;
|
pub use protocol::SyncMessage;
|
||||||
pub use quic::{QuicConfig, QuicConnection, QuicServer, quic_connect};
|
pub use quic::{QuicConfig, QuicConnection, QuicServer, quic_connect};
|
||||||
|
|||||||
@@ -21,10 +21,28 @@
|
|||||||
//! Messages are framed identically to the TCP transport (4-byte LE length
|
//! Messages are framed identically to the TCP transport (4-byte LE length
|
||||||
//! prefix) so the same `SyncMessage` serialization works unchanged.
|
//! prefix) so the same `SyncMessage` serialization works unchanged.
|
||||||
//!
|
//!
|
||||||
|
//! ## Bidirectional IPC
|
||||||
|
//!
|
||||||
|
//! Use **two** channel files for full-duplex communication between processes:
|
||||||
|
//! one file per direction. Create both with `MmapChannel::create`, then
|
||||||
|
//! split each into a `MmapSender` + `MmapReceiver` pair (or open them
|
||||||
|
//! directly). Feed the two halves into `SyncPeer::Mmap { sender, receiver }`
|
||||||
|
//! for a transport-agnostic sync session.
|
||||||
|
//!
|
||||||
//! ## Limitations
|
//! ## Limitations
|
||||||
//! - Single producer / single consumer (no concurrent senders).
|
//! - Single producer / single consumer per file (no concurrent senders).
|
||||||
//! - Capacity is fixed at creation time.
|
//! - Capacity is fixed at creation time.
|
||||||
//! - No cross-machine support (file must be on a shared filesystem).
|
//! - No cross-machine support (file must be on a shared filesystem).
|
||||||
|
//!
|
||||||
|
//! ## Atomicity
|
||||||
|
//!
|
||||||
|
//! `write_head` (offset 8) and `read_head` (offset 16) are updated with
|
||||||
|
//! `Acquire`/`Release` atomic stores so that concurrent readers in a
|
||||||
|
//! separate process see consistent pointer values without kernel involvement.
|
||||||
|
//! The ring-buffer body bytes are written before `write_head` is advanced,
|
||||||
|
//! ensuring the consumer never reads a partial frame.
|
||||||
|
|
||||||
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
|
||||||
use memmap2::MmapMut;
|
use memmap2::MmapMut;
|
||||||
use std::fs::OpenOptions;
|
use std::fs::OpenOptions;
|
||||||
@@ -57,16 +75,20 @@ pub struct MmapChannel {
|
|||||||
capacity: usize,
|
capacity: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send half of an `MmapChannel`.
|
/// Send half of a shared-memory channel.
|
||||||
|
///
|
||||||
|
/// Opened independently from the channel file; shares the same OS page
|
||||||
|
/// mapping. For bidirectional IPC use one `MmapSender` + one `MmapReceiver`
|
||||||
|
/// backed by **separate** files (one per direction).
|
||||||
pub struct MmapSender {
|
pub struct MmapSender {
|
||||||
_path: PathBuf,
|
mmap: MmapMut,
|
||||||
_capacity: usize,
|
capacity: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Receive half of an `MmapChannel`.
|
/// Receive half of a shared-memory channel.
|
||||||
pub struct MmapReceiver {
|
pub struct MmapReceiver {
|
||||||
_path: PathBuf,
|
mmap: MmapMut,
|
||||||
_capacity: usize,
|
capacity: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MmapChannel {
|
impl MmapChannel {
|
||||||
@@ -85,11 +107,11 @@ impl MmapChannel {
|
|||||||
|
|
||||||
let mut mmap = unsafe { MmapMut::map_mut(&file).map_err(TransportError::Io)? };
|
let mut mmap = unsafe { MmapMut::map_mut(&file).map_err(TransportError::Io)? };
|
||||||
|
|
||||||
// Write header
|
// Write header (no concurrent readers yet — plain stores are fine here)
|
||||||
mmap[..8].copy_from_slice(MAGIC);
|
mmap[..8].copy_from_slice(MAGIC);
|
||||||
write_u64(&mut mmap, 8, 0); // write_head
|
write_u64_plain(&mut mmap, 8, 0); // write_head
|
||||||
write_u64(&mut mmap, 16, 0); // read_head
|
write_u64_plain(&mut mmap, 16, 0); // read_head
|
||||||
write_u64(&mut mmap, 24, capacity as u64); // capacity
|
write_u64_plain(&mut mmap, 24, capacity as u64); // capacity
|
||||||
mmap.flush().map_err(TransportError::Io)?;
|
mmap.flush().map_err(TransportError::Io)?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
@@ -101,19 +123,7 @@ impl MmapChannel {
|
|||||||
|
|
||||||
/// Open an existing channel file.
|
/// Open an existing channel file.
|
||||||
pub fn open(path: &Path) -> Result<Self, TransportError> {
|
pub fn open(path: &Path) -> Result<Self, TransportError> {
|
||||||
let file = OpenOptions::new()
|
let (mmap, capacity) = open_mmap(path)?;
|
||||||
.read(true)
|
|
||||||
.write(true)
|
|
||||||
.open(path)
|
|
||||||
.map_err(TransportError::Io)?;
|
|
||||||
let mmap = unsafe { MmapMut::map_mut(&file).map_err(TransportError::Io)? };
|
|
||||||
|
|
||||||
if &mmap[..8] != MAGIC {
|
|
||||||
return Err(TransportError::Protocol(
|
|
||||||
"invalid mmap channel magic".into(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let capacity = read_u64(&mmap, 24) as usize;
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
path: path.to_owned(),
|
path: path.to_owned(),
|
||||||
mmap,
|
mmap,
|
||||||
@@ -125,148 +135,204 @@ impl MmapChannel {
|
|||||||
///
|
///
|
||||||
/// Blocks (spin-waits) if there is not enough space yet.
|
/// Blocks (spin-waits) if there is not enough space yet.
|
||||||
pub fn send(&mut self, msg: &SyncMessage) -> Result<(), TransportError> {
|
pub fn send(&mut self, msg: &SyncMessage) -> Result<(), TransportError> {
|
||||||
let frame = msg.to_frame().map_err(TransportError::Protocol)?;
|
ring_send(&mut self.mmap, self.capacity, msg)
|
||||||
let frame_len = frame.len();
|
|
||||||
if frame_len > self.capacity - 8 {
|
|
||||||
return Err(TransportError::Protocol(format!(
|
|
||||||
"message too large for mmap channel: {} bytes",
|
|
||||||
frame_len
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spin until space is available
|
|
||||||
loop {
|
|
||||||
let wh = read_u64(&self.mmap, 8) as usize;
|
|
||||||
let rh = read_u64(&self.mmap, 16) as usize;
|
|
||||||
let used = wh.wrapping_sub(rh);
|
|
||||||
let free = self.capacity.saturating_sub(used);
|
|
||||||
if free >= frame_len {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
std::hint::spin_loop();
|
|
||||||
}
|
|
||||||
|
|
||||||
let wh = read_u64(&self.mmap, 8) as usize;
|
|
||||||
let buf_off = HEADER_SIZE + (wh % self.capacity);
|
|
||||||
|
|
||||||
// Write frame (may wrap around)
|
|
||||||
self.write_ring(buf_off, &frame);
|
|
||||||
let new_wh = (wh + frame_len) as u64;
|
|
||||||
write_u64(&mut self.mmap, 8, new_wh);
|
|
||||||
self.mmap.flush_range(0, 32).map_err(TransportError::Io)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Receive the next message from the ring buffer.
|
/// Receive the next message from the ring buffer.
|
||||||
///
|
///
|
||||||
/// Blocks (spin-waits) until a complete message is available.
|
/// Blocks (spin-waits) until a complete message is available.
|
||||||
pub fn recv(&mut self) -> Result<SyncMessage, TransportError> {
|
pub fn recv(&mut self) -> Result<SyncMessage, TransportError> {
|
||||||
// Wait for at least 4 bytes (length prefix)
|
ring_recv(&mut self.mmap, self.capacity)
|
||||||
loop {
|
|
||||||
let wh = read_u64(&self.mmap, 8) as usize;
|
|
||||||
let rh = read_u64(&self.mmap, 16) as usize;
|
|
||||||
if wh.wrapping_sub(rh) >= 4 {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
std::hint::spin_loop();
|
|
||||||
}
|
|
||||||
|
|
||||||
let rh = read_u64(&self.mmap, 16) as usize;
|
|
||||||
let buf_off = HEADER_SIZE + (rh % self.capacity);
|
|
||||||
|
|
||||||
// Read 4-byte length
|
|
||||||
let mut len_bytes = [0u8; 4];
|
|
||||||
self.read_ring(buf_off, &mut len_bytes);
|
|
||||||
let msg_len = u32::from_le_bytes(len_bytes) as usize;
|
|
||||||
|
|
||||||
if msg_len > MAX_FRAME_SIZE {
|
|
||||||
return Err(TransportError::Protocol(format!(
|
|
||||||
"mmap frame too large: {msg_len}"
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for full message
|
|
||||||
loop {
|
|
||||||
let wh = read_u64(&self.mmap, 8) as usize;
|
|
||||||
let rh2 = read_u64(&self.mmap, 16) as usize;
|
|
||||||
if wh.wrapping_sub(rh2) >= 4 + msg_len {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
std::hint::spin_loop();
|
|
||||||
}
|
|
||||||
|
|
||||||
let body_off = HEADER_SIZE + ((rh + 4) % self.capacity);
|
|
||||||
let mut body = vec![0u8; msg_len];
|
|
||||||
self.read_ring(body_off, &mut body);
|
|
||||||
|
|
||||||
// Advance read head
|
|
||||||
let new_rh = (rh + 4 + msg_len) as u64;
|
|
||||||
write_u64(&mut self.mmap, 16, new_rh);
|
|
||||||
self.mmap.flush_range(0, 32).map_err(TransportError::Io)?;
|
|
||||||
|
|
||||||
SyncMessage::from_bytes(&body).map_err(TransportError::Protocol)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Path to the backing file.
|
/// Path to the backing file.
|
||||||
pub fn path(&self) -> &Path {
|
pub fn path(&self) -> &Path {
|
||||||
&self.path
|
&self.path
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
fn write_ring(&mut self, start: usize, data: &[u8]) {
|
|
||||||
let cap = self.capacity;
|
|
||||||
let rel = start - HEADER_SIZE;
|
|
||||||
for (i, &b) in data.iter().enumerate() {
|
|
||||||
let off = HEADER_SIZE + (rel + i) % cap;
|
|
||||||
self.mmap[off] = b;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_ring(&self, start: usize, buf: &mut [u8]) {
|
|
||||||
let cap = self.capacity;
|
|
||||||
let rel = start - HEADER_SIZE;
|
|
||||||
for (i, slot) in buf.iter_mut().enumerate() {
|
|
||||||
let off = HEADER_SIZE + (rel + i) % cap;
|
|
||||||
*slot = self.mmap[off];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
// Convenience: split into send/receive halves (file-based, for multi-process)
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
impl MmapSender {
|
impl MmapSender {
|
||||||
|
/// Open an existing channel file for writing (producer side).
|
||||||
pub fn open(path: &Path) -> Result<Self, TransportError> {
|
pub fn open(path: &Path) -> Result<Self, TransportError> {
|
||||||
let ch = MmapChannel::open(path)?;
|
let (mmap, capacity) = open_mmap(path)?;
|
||||||
Ok(Self {
|
Ok(Self { mmap, capacity })
|
||||||
_path: path.to_owned(),
|
}
|
||||||
_capacity: ch.capacity,
|
|
||||||
})
|
/// Send a `SyncMessage` into the ring buffer.
|
||||||
|
///
|
||||||
|
/// Blocks (spin-waits) if there is not enough space yet. For async
|
||||||
|
/// callers, wrap with `tokio::task::block_in_place`.
|
||||||
|
pub fn send(&mut self, msg: &SyncMessage) -> Result<(), TransportError> {
|
||||||
|
ring_send(&mut self.mmap, self.capacity, msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MmapReceiver {
|
impl MmapReceiver {
|
||||||
|
/// Open an existing channel file for reading (consumer side).
|
||||||
pub fn open(path: &Path) -> Result<Self, TransportError> {
|
pub fn open(path: &Path) -> Result<Self, TransportError> {
|
||||||
let ch = MmapChannel::open(path)?;
|
let (mmap, capacity) = open_mmap(path)?;
|
||||||
Ok(Self {
|
Ok(Self { mmap, capacity })
|
||||||
_path: path.to_owned(),
|
}
|
||||||
_capacity: ch.capacity,
|
|
||||||
})
|
/// Receive the next `SyncMessage` from the ring buffer.
|
||||||
|
///
|
||||||
|
/// Blocks (spin-waits) until a complete message is available. For async
|
||||||
|
/// callers, wrap with `tokio::task::block_in_place`.
|
||||||
|
pub fn recv(&mut self) -> Result<SyncMessage, TransportError> {
|
||||||
|
ring_recv(&mut self.mmap, self.capacity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Shared ring-buffer logic
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Open a channel file and return `(mmap, capacity)`.
|
||||||
|
fn open_mmap(path: &Path) -> Result<(MmapMut, usize), TransportError> {
|
||||||
|
let file = OpenOptions::new()
|
||||||
|
.read(true)
|
||||||
|
.write(true)
|
||||||
|
.open(path)
|
||||||
|
.map_err(TransportError::Io)?;
|
||||||
|
let mmap = unsafe { MmapMut::map_mut(&file).map_err(TransportError::Io)? };
|
||||||
|
|
||||||
|
if &mmap[..8] != MAGIC {
|
||||||
|
return Err(TransportError::Protocol(
|
||||||
|
"invalid mmap channel magic".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let capacity = read_u64_plain(&mmap, 24) as usize;
|
||||||
|
Ok((mmap, capacity))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ring_send(mmap: &mut MmapMut, capacity: usize, msg: &SyncMessage) -> Result<(), TransportError> {
|
||||||
|
let frame = msg.to_frame().map_err(TransportError::Protocol)?;
|
||||||
|
let frame_len = frame.len();
|
||||||
|
if frame_len > capacity - 8 {
|
||||||
|
return Err(TransportError::Protocol(format!(
|
||||||
|
"message too large for mmap channel: {} bytes",
|
||||||
|
frame_len
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spin until space is available
|
||||||
|
loop {
|
||||||
|
let wh = atomic_load(mmap, 8) as usize;
|
||||||
|
let rh = atomic_load(mmap, 16) as usize;
|
||||||
|
let used = wh.wrapping_sub(rh);
|
||||||
|
let free = capacity.saturating_sub(used);
|
||||||
|
if free >= frame_len {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
std::hint::spin_loop();
|
||||||
|
}
|
||||||
|
|
||||||
|
let wh = atomic_load(mmap, 8) as usize;
|
||||||
|
let buf_off = HEADER_SIZE + (wh % capacity);
|
||||||
|
|
||||||
|
// Write frame bytes into ring buffer, then advance write_head atomically.
|
||||||
|
// The Release store ensures the body bytes are visible before the pointer.
|
||||||
|
write_ring(mmap, buf_off, capacity, &frame);
|
||||||
|
atomic_store(mmap, 8, (wh + frame_len) as u64);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ring_recv(mmap: &mut MmapMut, capacity: usize) -> Result<SyncMessage, TransportError> {
|
||||||
|
// Wait for at least 4 bytes (length prefix)
|
||||||
|
loop {
|
||||||
|
let wh = atomic_load(mmap, 8) as usize;
|
||||||
|
let rh = atomic_load(mmap, 16) as usize;
|
||||||
|
if wh.wrapping_sub(rh) >= 4 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
std::hint::spin_loop();
|
||||||
|
}
|
||||||
|
|
||||||
|
let rh = atomic_load(mmap, 16) as usize;
|
||||||
|
let buf_off = HEADER_SIZE + (rh % capacity);
|
||||||
|
|
||||||
|
// Read 4-byte length
|
||||||
|
let mut len_bytes = [0u8; 4];
|
||||||
|
read_ring(mmap, buf_off, capacity, &mut len_bytes);
|
||||||
|
let msg_len = u32::from_le_bytes(len_bytes) as usize;
|
||||||
|
|
||||||
|
if msg_len > MAX_FRAME_SIZE {
|
||||||
|
return Err(TransportError::Protocol(format!(
|
||||||
|
"mmap frame too large: {msg_len}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for full message
|
||||||
|
loop {
|
||||||
|
let wh = atomic_load(mmap, 8) as usize;
|
||||||
|
let rh2 = atomic_load(mmap, 16) as usize;
|
||||||
|
if wh.wrapping_sub(rh2) >= 4 + msg_len {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
std::hint::spin_loop();
|
||||||
|
}
|
||||||
|
|
||||||
|
let body_off = HEADER_SIZE + ((rh + 4) % capacity);
|
||||||
|
let mut body = vec![0u8; msg_len];
|
||||||
|
read_ring(mmap, body_off, capacity, &mut body);
|
||||||
|
|
||||||
|
// Advance read head atomically
|
||||||
|
atomic_store(mmap, 16, (rh + 4 + msg_len) as u64);
|
||||||
|
|
||||||
|
SyncMessage::from_bytes(&body).map_err(TransportError::Protocol)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Ring-buffer byte helpers
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fn write_ring(mmap: &mut MmapMut, start: usize, cap: usize, data: &[u8]) {
|
||||||
|
let rel = start - HEADER_SIZE;
|
||||||
|
for (i, &b) in data.iter().enumerate() {
|
||||||
|
let off = HEADER_SIZE + (rel + i) % cap;
|
||||||
|
mmap[off] = b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_ring(mmap: &MmapMut, start: usize, cap: usize, buf: &mut [u8]) {
|
||||||
|
let rel = start - HEADER_SIZE;
|
||||||
|
for (i, slot) in buf.iter_mut().enumerate() {
|
||||||
|
let off = HEADER_SIZE + (rel + i) % cap;
|
||||||
|
*slot = mmap[off];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// Atomic header helpers
|
// Atomic header helpers
|
||||||
|
//
|
||||||
|
// `write_head` (offset 8) and `read_head` (offset 16) are accessed via
|
||||||
|
// AtomicU64 pointer casts. Both offsets are 8-byte aligned in a page-aligned
|
||||||
|
// mmap, so the cast is sound on all supported targets.
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn read_u64(mmap: &[u8], off: usize) -> u64 {
|
/// Acquire-load a u64 from the mmap header at `off`.
|
||||||
|
fn atomic_load(mmap: &MmapMut, off: usize) -> u64 {
|
||||||
|
// SAFETY: mmap is page-aligned; off ∈ {8, 16} → 8-byte aligned; in range.
|
||||||
|
let ptr = unsafe { &*(mmap.as_ptr().add(off) as *const AtomicU64) };
|
||||||
|
ptr.load(Ordering::Acquire)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Release-store a u64 into the mmap header at `off`.
|
||||||
|
fn atomic_store(mmap: &MmapMut, off: usize, val: u64) {
|
||||||
|
// SAFETY: same as atomic_load.
|
||||||
|
let ptr = unsafe { &*(mmap.as_ptr().add(off) as *const AtomicU64) };
|
||||||
|
ptr.store(val, Ordering::Release);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Non-atomic plain read — used only during initialisation before any
|
||||||
|
/// concurrent access begins.
|
||||||
|
fn read_u64_plain(mmap: &[u8], off: usize) -> u64 {
|
||||||
let bytes: [u8; 8] = mmap[off..off + 8].try_into().unwrap_or([0u8; 8]);
|
let bytes: [u8; 8] = mmap[off..off + 8].try_into().unwrap_or([0u8; 8]);
|
||||||
u64::from_le_bytes(bytes)
|
u64::from_le_bytes(bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_u64(mmap: &mut [u8], off: usize, val: u64) {
|
/// Non-atomic plain write — used only during initialisation.
|
||||||
|
fn write_u64_plain(mmap: &mut [u8], off: usize, val: u64) {
|
||||||
mmap[off..off + 8].copy_from_slice(&val.to_le_bytes());
|
mmap[off..off + 8].copy_from_slice(&val.to_le_bytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,6 +352,8 @@ mod tests {
|
|||||||
(f, p)
|
(f, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── MmapChannel (combined) ────────────────────────────────────────────────
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn create_and_open() {
|
fn create_and_open() {
|
||||||
let (_f, path) = tmp_path();
|
let (_f, path) = tmp_path();
|
||||||
@@ -376,4 +444,89 @@ mod tests {
|
|||||||
let result = MmapChannel::open(&path);
|
let result = MmapChannel::open(&path);
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── MmapSender / MmapReceiver (split halves) ──────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sender_receiver_open_same_file() {
|
||||||
|
let (_f, path) = tmp_path();
|
||||||
|
let _ch = MmapChannel::create(&path, DEFAULT_CAPACITY).unwrap();
|
||||||
|
let _tx = MmapSender::open(&path).unwrap();
|
||||||
|
let _rx = MmapReceiver::open(&path).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sender_receiver_roundtrip() {
|
||||||
|
let (_f, path) = tmp_path();
|
||||||
|
MmapChannel::create(&path, DEFAULT_CAPACITY).unwrap();
|
||||||
|
|
||||||
|
let mut tx = MmapSender::open(&path).unwrap();
|
||||||
|
let mut rx = MmapReceiver::open(&path).unwrap();
|
||||||
|
|
||||||
|
tx.send(&SyncMessage::Ack { revision: 7 }).unwrap();
|
||||||
|
let msg = rx.recv().unwrap();
|
||||||
|
assert!(matches!(msg, SyncMessage::Ack { revision: 7 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sender_receiver_multiple_messages() {
|
||||||
|
let (_f, path) = tmp_path();
|
||||||
|
MmapChannel::create(&path, DEFAULT_CAPACITY).unwrap();
|
||||||
|
|
||||||
|
let mut tx = MmapSender::open(&path).unwrap();
|
||||||
|
let mut rx = MmapReceiver::open(&path).unwrap();
|
||||||
|
|
||||||
|
for i in 0u64..8 {
|
||||||
|
tx.send(&SyncMessage::Ack { revision: i }).unwrap();
|
||||||
|
}
|
||||||
|
for i in 0u64..8 {
|
||||||
|
match rx.recv().unwrap() {
|
||||||
|
SyncMessage::Ack { revision } => assert_eq!(revision, i),
|
||||||
|
other => panic!("unexpected: {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sender_receiver_ring_wraps() {
|
||||||
|
let cap = 1024;
|
||||||
|
let (_f, path) = tmp_path();
|
||||||
|
MmapChannel::create(&path, cap).unwrap();
|
||||||
|
|
||||||
|
let mut tx = MmapSender::open(&path).unwrap();
|
||||||
|
let mut rx = MmapReceiver::open(&path).unwrap();
|
||||||
|
|
||||||
|
for i in 0u64..20 {
|
||||||
|
tx.send(&SyncMessage::Ack { revision: i }).unwrap();
|
||||||
|
let recv = rx.recv().unwrap();
|
||||||
|
assert!(matches!(recv, SyncMessage::Ack { revision } if revision == i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Two-thread test: sender and receiver on the same channel from separate
|
||||||
|
/// threads, verifying the atomic pointer updates work correctly under
|
||||||
|
/// concurrent access.
|
||||||
|
#[test]
|
||||||
|
fn sender_receiver_cross_thread() {
|
||||||
|
let (_f, path) = tmp_path();
|
||||||
|
MmapChannel::create(&path, DEFAULT_CAPACITY).unwrap();
|
||||||
|
|
||||||
|
let path_tx = path.clone();
|
||||||
|
let tx_thread = std::thread::spawn(move || {
|
||||||
|
let mut tx = MmapSender::open(&path_tx).unwrap();
|
||||||
|
for i in 0u64..16 {
|
||||||
|
tx.send(&SyncMessage::Ack { revision: i }).unwrap();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut rx = MmapReceiver::open(&path).unwrap();
|
||||||
|
for i in 0u64..16 {
|
||||||
|
match rx.recv().unwrap() {
|
||||||
|
SyncMessage::Ack { revision } => assert_eq!(revision, i),
|
||||||
|
other => panic!("unexpected: {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tx_thread.join().unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,12 @@
|
|||||||
//! - **TCP** → `OwnedReadHalf` / `OwnedWriteHalf` via `TcpConnection::into_split()`
|
//! - **TCP** → `OwnedReadHalf` / `OwnedWriteHalf` via `TcpConnection::into_split()`
|
||||||
//! - **QUIC** → two `Arc<QuicConnection>` clones (both halves share the connection
|
//! - **QUIC** → two `Arc<QuicConnection>` clones (both halves share the connection
|
||||||
//! since QUIC `send`/`recv` are `&self`)
|
//! since QUIC `send`/`recv` are `&self`)
|
||||||
|
//! - **Mmap** → `MmapSender` / `MmapReceiver` (already independent ring buffers)
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::error::TransportError;
|
use crate::error::TransportError;
|
||||||
|
use crate::mmap::{MmapReceiver, MmapSender};
|
||||||
use crate::protocol::SyncMessage;
|
use crate::protocol::SyncMessage;
|
||||||
use crate::quic::QuicConnection;
|
use crate::quic::QuicConnection;
|
||||||
use crate::tcp::{TcpConnection, TcpReadHalf, TcpWriteHalf};
|
use crate::tcp::{TcpConnection, TcpReadHalf, TcpWriteHalf};
|
||||||
@@ -22,10 +24,17 @@ use crate::tcp::{TcpConnection, TcpReadHalf, TcpWriteHalf};
|
|||||||
// SyncPeer
|
// SyncPeer
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// A unified connection handle for a single sync session (TCP or QUIC).
|
/// A unified connection handle for a single sync session (TCP, QUIC, or mmap).
|
||||||
pub enum SyncPeer {
|
pub enum SyncPeer {
|
||||||
Tcp(TcpConnection),
|
Tcp(TcpConnection),
|
||||||
Quic(Arc<QuicConnection>),
|
Quic(Arc<QuicConnection>),
|
||||||
|
/// Local same-node transport backed by two memory-mapped ring buffers —
|
||||||
|
/// one per direction. `send_ch` is the outgoing channel; `recv_ch` is
|
||||||
|
/// the incoming channel.
|
||||||
|
Mmap {
|
||||||
|
send_ch: MmapSender,
|
||||||
|
recv_ch: MmapReceiver,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SyncPeer {
|
impl SyncPeer {
|
||||||
@@ -33,6 +42,9 @@ impl SyncPeer {
|
|||||||
match self {
|
match self {
|
||||||
SyncPeer::Tcp(c) => c.send(msg).await,
|
SyncPeer::Tcp(c) => c.send(msg).await,
|
||||||
SyncPeer::Quic(c) => c.send(msg).await,
|
SyncPeer::Quic(c) => c.send(msg).await,
|
||||||
|
SyncPeer::Mmap { send_ch, .. } => {
|
||||||
|
tokio::task::block_in_place(|| send_ch.send(msg))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,6 +52,9 @@ impl SyncPeer {
|
|||||||
match self {
|
match self {
|
||||||
SyncPeer::Tcp(c) => c.recv().await,
|
SyncPeer::Tcp(c) => c.recv().await,
|
||||||
SyncPeer::Quic(c) => c.recv().await,
|
SyncPeer::Quic(c) => c.recv().await,
|
||||||
|
SyncPeer::Mmap { recv_ch, .. } => {
|
||||||
|
tokio::task::block_in_place(|| recv_ch.recv())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,6 +65,7 @@ impl SyncPeer {
|
|||||||
c.close();
|
c.close();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
SyncPeer::Mmap { .. } => Ok(()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +73,7 @@ impl SyncPeer {
|
|||||||
///
|
///
|
||||||
/// TCP: yields `OwnedReadHalf` / `OwnedWriteHalf` via `into_split()`.
|
/// TCP: yields `OwnedReadHalf` / `OwnedWriteHalf` via `into_split()`.
|
||||||
/// QUIC: yields two `Arc` clones — both halves share the connection.
|
/// QUIC: yields two `Arc` clones — both halves share the connection.
|
||||||
|
/// Mmap: yields the already-independent `MmapSender` / `MmapReceiver`.
|
||||||
pub fn into_pipe_halves(self) -> (PipeReadHalf, PipeWriteHalf) {
|
pub fn into_pipe_halves(self) -> (PipeReadHalf, PipeWriteHalf) {
|
||||||
match self {
|
match self {
|
||||||
SyncPeer::Tcp(conn) => {
|
SyncPeer::Tcp(conn) => {
|
||||||
@@ -64,6 +81,9 @@ impl SyncPeer {
|
|||||||
(PipeReadHalf::Tcp(r), PipeWriteHalf::Tcp(w))
|
(PipeReadHalf::Tcp(r), PipeWriteHalf::Tcp(w))
|
||||||
}
|
}
|
||||||
SyncPeer::Quic(arc) => (PipeReadHalf::Quic(arc.clone()), PipeWriteHalf::Quic(arc)),
|
SyncPeer::Quic(arc) => (PipeReadHalf::Quic(arc.clone()), PipeWriteHalf::Quic(arc)),
|
||||||
|
SyncPeer::Mmap { send_ch, recv_ch } => {
|
||||||
|
(PipeReadHalf::Mmap(recv_ch), PipeWriteHalf::Mmap(send_ch))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,6 +96,7 @@ impl SyncPeer {
|
|||||||
pub enum PipeWriteHalf {
|
pub enum PipeWriteHalf {
|
||||||
Tcp(TcpWriteHalf),
|
Tcp(TcpWriteHalf),
|
||||||
Quic(Arc<QuicConnection>),
|
Quic(Arc<QuicConnection>),
|
||||||
|
Mmap(MmapSender),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PipeWriteHalf {
|
impl PipeWriteHalf {
|
||||||
@@ -83,6 +104,7 @@ impl PipeWriteHalf {
|
|||||||
match self {
|
match self {
|
||||||
PipeWriteHalf::Tcp(h) => h.send(msg).await,
|
PipeWriteHalf::Tcp(h) => h.send(msg).await,
|
||||||
PipeWriteHalf::Quic(c) => c.send(msg).await,
|
PipeWriteHalf::Quic(c) => c.send(msg).await,
|
||||||
|
PipeWriteHalf::Mmap(tx) => tokio::task::block_in_place(|| tx.send(msg)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,6 +115,7 @@ impl PipeWriteHalf {
|
|||||||
c.close();
|
c.close();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
PipeWriteHalf::Mmap(_) => Ok(()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -101,6 +124,7 @@ impl PipeWriteHalf {
|
|||||||
pub enum PipeReadHalf {
|
pub enum PipeReadHalf {
|
||||||
Tcp(TcpReadHalf),
|
Tcp(TcpReadHalf),
|
||||||
Quic(Arc<QuicConnection>),
|
Quic(Arc<QuicConnection>),
|
||||||
|
Mmap(MmapReceiver),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PipeReadHalf {
|
impl PipeReadHalf {
|
||||||
@@ -108,6 +132,7 @@ impl PipeReadHalf {
|
|||||||
match self {
|
match self {
|
||||||
PipeReadHalf::Tcp(h) => h.recv().await,
|
PipeReadHalf::Tcp(h) => h.recv().await,
|
||||||
PipeReadHalf::Quic(c) => c.recv().await,
|
PipeReadHalf::Quic(c) => c.recv().await,
|
||||||
|
PipeReadHalf::Mmap(rx) => tokio::task::block_in_place(|| rx.recv()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user