use std::{collections::VecDeque, error::Error, pin::Pin, task::Poll};
use bytes::Bytes;
use futures::{SinkExt as _, StreamExt as _};
use snafu::Snafu;
use crate::{
quic::{self, GetStreamIdExt as _},
rpc::stream::{
frame::{WriteCommand, WriteEvent},
io::FrameIo,
},
varint::VarInt,
};
#[derive(Debug, Snafu)]
#[snafu(module)]
enum WriteHypervisorProtocolError {
#[snafu(display("received reset code {actual} after committed reset code {expected}"))]
ConflictingReset { expected: VarInt, actual: VarInt },
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum HyperWriteJob {
Push { data: Bytes },
Flush,
Eos,
Reset { code: VarInt },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum HyperWriteDone {
Push,
Flush,
Eos,
Reset { code: VarInt },
}
pub(crate) async fn run_write_bridge<W, Io, E>(mut writer: W, mut bridge: Io)
where
W: quic::WriteStream + Unpin,
Io: FrameIo<WriteEvent, WriteCommand, E> + Send + Unpin,
E: Error + Send + 'static,
{
let stream_id = match writer.stream_id().await {
Ok(stream_id) => Some(stream_id),
Err(error) => {
tracing::warn!(?error, "QUIC write bridge could not resolve stream id");
None
}
};
send_write_event(&mut bridge, WriteEvent::Pull).await;
let mut queue = VecDeque::new();
let mut reset_code = None;
let mut inbound_closed = false;
let mut credit_outstanding = true;
let mut push_sequence = 0_u64;
loop {
if let Some(job) = queue.pop_front() {
match &job {
HyperWriteJob::Push { data } => {
push_sequence += 1;
tracing::trace!(
stream_id = ?stream_id.map(|id| id.into_inner()),
sequence = push_sequence,
bytes = data.len(),
"QUIC write bridge push started"
);
}
HyperWriteJob::Flush => {
tracing::trace!(
stream_id = ?stream_id.map(|id| id.into_inner()),
sequence = push_sequence,
"QUIC write bridge flush started"
);
}
HyperWriteJob::Reset { code } => {
tracing::trace!(
boundary = "quic-root",
stream_id = ?stream_id.map(|id| id.into_inner()),
code = code.into_inner(),
"QUIC write bridge reset started"
);
}
HyperWriteJob::Eos => {
tracing::trace!(
boundary = "quic-root",
stream_id = ?stream_id.map(|id| id.into_inner()),
"QUIC write bridge EOS started"
);
}
}
let done = {
let mut current = Box::pin(run_write_job(&mut writer, job));
loop {
tokio::select! {
result = &mut current => break result,
inbound = bridge.next(), if !inbound_closed => {
match inbound {
Some(Ok(command)) => {
if !record_command(
&mut queue,
&mut reset_code,
&mut credit_outstanding,
command,
) {
return;
}
}
Some(Err(error)) => {
let report = snafu::Report::from_error(&error);
tracing::warn!(error = %report, "stream frame write bridge input failed");
return;
}
None => {
inbound_closed = true;
}
}
}
}
}
};
match done {
Ok(HyperWriteDone::Push) => {
tracing::trace!(
stream_id = ?stream_id.map(|id| id.into_inner()),
sequence = push_sequence,
"QUIC write bridge push completed"
);
if should_send_credit(inbound_closed, reset_code, &queue, credit_outstanding)
&& !send_write_credit(
&mut bridge,
&mut queue,
&mut reset_code,
&mut inbound_closed,
&mut credit_outstanding,
)
.await
{
return;
}
}
Ok(HyperWriteDone::Flush) => {
tracing::trace!(
stream_id = ?stream_id.map(|id| id.into_inner()),
sequence = push_sequence,
"QUIC write bridge flush completed"
);
send_write_event(&mut bridge, WriteEvent::FlushAck).await;
if should_send_credit(inbound_closed, reset_code, &queue, credit_outstanding)
&& !send_write_credit(
&mut bridge,
&mut queue,
&mut reset_code,
&mut inbound_closed,
&mut credit_outstanding,
)
.await
{
return;
}
}
Ok(HyperWriteDone::Eos) => {
tracing::trace!(
boundary = "quic-root",
stream_id = ?stream_id.map(|id| id.into_inner()),
"QUIC write bridge EOS completed"
);
send_write_event(&mut bridge, WriteEvent::EosAck).await;
return;
}
Ok(HyperWriteDone::Reset { code }) => {
tracing::trace!(
boundary = "quic-root",
stream_id = ?stream_id.map(|id| id.into_inner()),
code = code.into_inner(),
"QUIC write bridge reset completed"
);
send_write_event(&mut bridge, WriteEvent::ResetAck { code }).await;
return;
}
Err(quic::StreamError::Reset { code }) => {
send_write_event(&mut bridge, WriteEvent::ErrReset { code }).await;
return;
}
Err(quic::StreamError::Connection { .. }) => {
send_write_event(&mut bridge, WriteEvent::ErrConn).await;
return;
}
}
continue;
}
if inbound_closed {
return;
}
match bridge.next().await {
Some(Ok(command)) => {
if !record_command(
&mut queue,
&mut reset_code,
&mut credit_outstanding,
command,
) {
return;
}
}
Some(Err(error)) => {
let report = snafu::Report::from_error(&error);
tracing::warn!(error = %report, "stream frame write bridge input failed");
return;
}
None => inbound_closed = true,
}
}
}
fn record_command(
queue: &mut VecDeque<HyperWriteJob>,
reset_code: &mut Option<VarInt>,
credit_outstanding: &mut bool,
command: WriteCommand,
) -> bool {
match command {
WriteCommand::Push { data } => {
*credit_outstanding = false;
if reset_code.is_none() && !has_terminal(queue) {
queue.push_back(HyperWriteJob::Push { data });
}
true
}
WriteCommand::Flush => {
if reset_code.is_none() && !has_terminal(queue) {
queue.push_back(HyperWriteJob::Flush);
}
true
}
WriteCommand::Eos => {
if reset_code.is_none() && !has_terminal(queue) {
queue.push_back(HyperWriteJob::Eos);
}
true
}
WriteCommand::Reset { code } => match *reset_code {
Some(committed) if committed == code => true,
Some(expected) => {
let error = WriteHypervisorProtocolError::ConflictingReset {
expected,
actual: code,
};
let report = snafu::Report::from_error(&error);
tracing::warn!(error = %report, "stream frame write bridge input failed");
false
}
None => {
*reset_code = Some(code);
queue.clear();
queue.push_back(HyperWriteJob::Reset { code });
true
}
},
}
}
fn has_terminal(queue: &VecDeque<HyperWriteJob>) -> bool {
queue
.iter()
.any(|job| matches!(job, HyperWriteJob::Eos | HyperWriteJob::Reset { .. }))
}
fn should_send_credit(
inbound_closed: bool,
reset_code: Option<VarInt>,
queue: &VecDeque<HyperWriteJob>,
credit_outstanding: bool,
) -> bool {
!credit_outstanding && !inbound_closed && reset_code.is_none() && !has_terminal(queue)
}
async fn run_write_job<W>(
writer: &mut W,
job: HyperWriteJob,
) -> Result<HyperWriteDone, quic::StreamError>
where
W: quic::WriteStream + Unpin,
{
match job {
HyperWriteJob::Push { data } => {
writer.send(data).await?;
Ok(HyperWriteDone::Push)
}
HyperWriteJob::Flush => {
writer.flush().await?;
Ok(HyperWriteDone::Flush)
}
HyperWriteJob::Eos => {
writer.close().await?;
Ok(HyperWriteDone::Eos)
}
HyperWriteJob::Reset { code } => {
quic::ResetStreamExt::reset(writer, code).await?;
Ok(HyperWriteDone::Reset { code })
}
}
}
async fn send_write_event<Io, E>(bridge: &mut Io, event: WriteEvent)
where
Io: FrameIo<WriteEvent, WriteCommand, E> + Unpin,
E: Error + 'static,
{
if let Err(error) = bridge.send(event).await {
let report = snafu::Report::from_error(&error);
tracing::debug!(error = %report, "stream frame write bridge output failed");
}
}
enum CreditSendState {
Ready,
Flush,
Done,
}
async fn send_write_credit<Io, E>(
bridge: &mut Io,
queue: &mut VecDeque<HyperWriteJob>,
reset_code: &mut Option<VarInt>,
inbound_closed: &mut bool,
credit_outstanding: &mut bool,
) -> bool
where
Io: FrameIo<WriteEvent, WriteCommand, E> + Unpin,
E: Error + 'static,
{
let mut state = CreditSendState::Ready;
futures::future::poll_fn(|cx| {
if !*inbound_closed {
match Pin::new(&mut *bridge).poll_next(cx) {
Poll::Ready(Some(Ok(command))) => {
if record_command(queue, reset_code, credit_outstanding, command) {
return Poll::Ready(true);
}
return Poll::Ready(false);
}
Poll::Ready(Some(Err(error))) => {
let report = snafu::Report::from_error(&error);
tracing::warn!(error = %report, "stream frame write bridge input failed");
return Poll::Ready(false);
}
Poll::Ready(None) => {
*inbound_closed = true;
return Poll::Ready(true);
}
Poll::Pending => {}
}
}
loop {
match state {
CreditSendState::Ready => match Pin::new(&mut *bridge).poll_ready(cx) {
Poll::Ready(Ok(())) => {
if let Err(error) = Pin::new(&mut *bridge).start_send(WriteEvent::Pull) {
let report = snafu::Report::from_error(&error);
tracing::debug!(
error = %report,
"stream frame write bridge output failed"
);
state = CreditSendState::Done;
return Poll::Ready(true);
}
*credit_outstanding = true;
state = CreditSendState::Flush;
}
Poll::Ready(Err(error)) => {
let report = snafu::Report::from_error(&error);
tracing::debug!(error = %report, "stream frame write bridge output failed");
state = CreditSendState::Done;
return Poll::Ready(true);
}
Poll::Pending => return Poll::Pending,
},
CreditSendState::Flush => match Pin::new(&mut *bridge).poll_flush(cx) {
Poll::Ready(Ok(())) => {
state = CreditSendState::Done;
return Poll::Ready(true);
}
Poll::Ready(Err(error)) => {
let report = snafu::Report::from_error(&error);
tracing::debug!(error = %report, "stream frame write bridge output failed");
state = CreditSendState::Done;
return Poll::Ready(true);
}
Poll::Pending => return Poll::Pending,
},
CreditSendState::Done => return Poll::Ready(true),
}
}
})
.await
}