use std::error::Error;
use std::io::Write;
use std::sync::mpsc::RecvTimeoutError;
use std::time::{Duration, Instant};
use liminal::protocol::{Frame, ProtocolError, ProtocolVersion, decode as decode_generic};
use liminal_protocol::wire::{
ClientRequest, EnrollmentRequest, EnrollmentToken, Generation, PARTICIPANT_FRAME_TYPE,
ParticipantFrame, ParticipantId, ReceiverDirection, RecordAdmission,
RecordAdmissionAttemptToken, ServerValue, decode as decode_participant,
};
use crate::server::connection::LoopbackClientEnd;
use crate::server::participant::PARTICIPANT_CAPABILITY_BIT;
use super::{SocketFixture, encode_frame, encode_request};
const PARKED_CONVERSATION: u64 = 0x50_06;
const BUSY_CONVERSATION: u64 = 0x50_07;
const PIN_RING_BYTES: usize = 64 * 1024;
const PIN_DEADLINE: Duration = Duration::from_secs(5);
const IDLE_WINDOW: Duration = Duration::from_millis(200);
fn read_frame(
client: &mut LoopbackClientEnd,
buffer: &mut Vec<u8>,
) -> Result<Frame, Box<dyn Error>> {
let deadline = Instant::now() + PIN_DEADLINE;
loop {
match decode_generic(buffer) {
Ok((frame, consumed)) => {
buffer.drain(..consumed);
return Ok(frame);
}
Err(
ProtocolError::IncompleteHeader { .. } | ProtocolError::TruncatedPayload { .. },
) => {}
Err(error) => return Err(Box::new(error)),
}
let remaining = deadline
.checked_duration_since(Instant::now())
.ok_or("the loopback idle pin timed out waiting for a frame")?;
let mut chunk = [0_u8; 4096];
let read = client.read_timeout(&mut chunk, Some(remaining))?;
if read == 0 {
return Err("the loopback connection reached end of file".into());
}
buffer.extend_from_slice(chunk.get(..read).unwrap_or(&[]));
}
}
fn handshake(client: &mut LoopbackClientEnd, buffer: &mut Vec<u8>) -> Result<(), Box<dyn Error>> {
client.write_all(&encode_frame(&Frame::Connect {
flags: 0,
min_version: ProtocolVersion::new(1, 0),
max_version: ProtocolVersion::new(1, 0),
auth_token: Vec::new(),
})?)?;
let ack = read_frame(client, buffer)?;
if !matches!(
ack,
Frame::ConnectAck { capabilities, .. } if capabilities == PARTICIPANT_CAPABILITY_BIT
) {
return Err(
format!("the in-process connection was not participant-capable: {ack:?}").into(),
);
}
Ok(())
}
fn request(
client: &mut LoopbackClientEnd,
buffer: &mut Vec<u8>,
request: ClientRequest,
) -> Result<ServerValue, Box<dyn Error>> {
client.write_all(&encode_request(request)?)?;
loop {
let frame = read_frame(client, buffer)?;
let Frame::Unknown {
type_id: PARTICIPANT_FRAME_TYPE,
..
} = frame
else {
return Err(format!("expected a participant frame, got {frame:?}").into());
};
let bytes = encode_frame(&frame)?;
match decode_participant(&bytes, ReceiverDirection::Client)
.map_err(|error| format!("{error:?}"))?
{
ParticipantFrame::ServerValue(value) => return Ok(value),
ParticipantFrame::ServerPush(_) => {}
ParticipantFrame::ClientRequest(unexpected) => {
return Err(
format!("a client received a ClientRequest frame: {unexpected:?}").into(),
);
}
}
}
}
fn commit_records_for_idle_window(
busy: &mut LoopbackClientEnd,
busy_buffer: &mut Vec<u8>,
busy_participant: ParticipantId,
) -> Result<u32, Box<dyn Error>> {
let deadline = Instant::now() + IDLE_WINDOW;
let mut committed = 0_u32;
while Instant::now() < deadline {
let outcome = request(
busy,
busy_buffer,
ClientRequest::RecordAdmission(RecordAdmission {
conversation_id: BUSY_CONVERSATION,
participant_id: busy_participant,
capability_generation: Generation::ONE,
record_admission_attempt_token: RecordAdmissionAttemptToken::new(
[u8::try_from(committed % 251).unwrap_or(0); 16],
),
payload: vec![0xB5; 32],
}),
)?;
if !matches!(outcome, ServerValue::RecordCommitted(_)) {
return Err(format!("the busy connection's record did not commit: {outcome:?}").into());
}
committed = committed.saturating_add(1);
}
assert!(
committed > 0,
"the control committed no records, so the window it is supposed to make busy \
was empty and the flat reading below would prove nothing"
);
Ok(committed)
}
#[test]
fn a_parked_loopback_connection_costs_no_slices_while_another_one_works()
-> Result<(), Box<dyn Error>> {
let home = tempfile::tempdir()?;
let server = SocketFixture::start(&home.path().join("loopback-idle"))?;
let (mut parked, parked_connection) = server.spawn_loopback(PIN_RING_BYTES)?;
let parked_pid = parked_connection.pid();
let mut parked_buffer = Vec::new();
handshake(&mut parked, &mut parked_buffer)?;
let park_marker = server.observe_next_park(parked_pid);
let enrolled = request(
&mut parked,
&mut parked_buffer,
ClientRequest::Enrollment(EnrollmentRequest {
conversation_id: PARKED_CONVERSATION,
enrollment_token: EnrollmentToken::new([0x60; 16]),
}),
)?;
let ServerValue::EnrollBound(bound) = enrolled else {
return Err(format!("the parked connection did not enroll: {enrolled:?}").into());
};
assert_eq!(bound.capability_generation(), Generation::ONE);
park_marker
.recv_timeout(PIN_DEADLINE)
.map_err(|error| format!("the parked connection never reported its park: {error}"))?;
let parked_at = server
.observe_settled_park(parked_pid)
.recv_timeout(PIN_DEADLINE)
.map_err(|error| format!("the parked connection never settled after its park: {error}"))?;
assert_eq!(
server.slice_count(parked_pid),
parked_at,
"the settled park count must be the parked connection's slice count"
);
let unexpected_slice = server.observe_next_slice(parked_pid);
let (mut busy, busy_connection) = server.spawn_loopback(PIN_RING_BYTES)?;
let busy_pid = busy_connection.pid();
let mut busy_buffer = Vec::new();
handshake(&mut busy, &mut busy_buffer)?;
let busy_slices_before = server.slice_count(busy_pid);
let busy_enrolled = request(
&mut busy,
&mut busy_buffer,
ClientRequest::Enrollment(EnrollmentRequest {
conversation_id: BUSY_CONVERSATION,
enrollment_token: EnrollmentToken::new([0x61; 16]),
}),
)?;
let ServerValue::EnrollBound(busy_bound) = busy_enrolled else {
return Err(format!("the busy connection did not enroll: {busy_enrolled:?}").into());
};
let committed =
commit_records_for_idle_window(&mut busy, &mut busy_buffer, busy_bound.participant_id())?;
let busy_slices_after = server.slice_count(busy_pid);
assert!(
busy_slices_after > busy_slices_before,
"the unrelated in-process connection's slice count did not grow \
({busy_slices_before} -> {busy_slices_after}) across a window in which it \
committed {committed} records; the scheduler was not running, so the parked \
reading below would be meaningless"
);
assert!(
matches!(
unexpected_slice.recv_timeout(Duration::from_millis(100)),
Err(RecvTimeoutError::Timeout)
),
"the parked in-process connection serviced a slice with nothing to read — it \
polled its ring instead of waiting to be told"
);
assert_eq!(
server.slice_count(parked_pid),
parked_at,
"the parked in-process connection's slice count moved from {parked_at} while a \
second in-process connection committed {committed} records in another \
conversation; a parked loopback must cost nothing"
);
drop(parked);
drop(busy);
drop(parked_connection);
drop(busy_connection);
server.stop();
Ok(())
}