use bytes::Bytes;
use pingora_error::{Error, ErrorType::*, Result};
use std::future::Future;
use std::pin::{pin, Pin};
use std::task::{ready, Context, Poll};
use tokio::io::{AsyncWrite, AsyncWriteExt};
enum HeaderWriteState {
Idle,
Writing(usize, Bytes),
Flushing(usize),
Done,
TimedOut,
}
impl std::fmt::Debug for HeaderWriteState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
HeaderWriteState::Idle => write!(f, "Idle"),
HeaderWriteState::Writing(size, _) => write!(f, "Writing(size: {})", size),
HeaderWriteState::Flushing(size) => write!(f, "Flushing(size: {})", size),
HeaderWriteState::Done => write!(f, "Done"),
HeaderWriteState::TimedOut => write!(f, "TimedOut"),
}
}
}
struct SendHeaderState {
pending_header: Option<Bytes>,
should_flush: bool,
write_state: HeaderWriteState,
timeout_duration: Option<std::time::Duration>,
timeout_fut: Option<std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send + Sync>>>,
}
impl std::fmt::Debug for SendHeaderState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SendHeaderState")
.field("pending_header", &self.pending_header)
.field("should_flush", &self.should_flush)
.field("write_state", &self.write_state)
.field("timeout_duration", &self.timeout_duration)
.field(
"timeout_fut",
&self.timeout_fut.as_ref().map(|_| "Some(Future)"),
)
.finish()
}
}
impl SendHeaderState {
fn new() -> Self {
SendHeaderState {
pending_header: None,
should_flush: false,
write_state: HeaderWriteState::Idle,
timeout_duration: None,
timeout_fut: None,
}
}
}
pub struct HeaderWriter {
send_header_state: Box<SendHeaderState>,
}
impl Default for HeaderWriter {
fn default() -> Self {
Self::new()
}
}
impl HeaderWriter {
pub fn new() -> Self {
HeaderWriter {
send_header_state: Box::new(SendHeaderState::new()),
}
}
#[cfg(test)]
pub fn has_pending_header_task(&self) -> bool {
self.send_header_state.pending_header.is_some()
|| !matches!(
self.send_header_state.write_state,
HeaderWriteState::Idle | HeaderWriteState::Done | HeaderWriteState::TimedOut
)
}
pub fn send_header_task(
&mut self,
header_bytes: Bytes,
should_flush: bool,
timeout: Option<std::time::Duration>,
) {
assert!(
matches!(
self.send_header_state.write_state,
HeaderWriteState::Idle | HeaderWriteState::Done
),
"send_header_task called while previous task is still in progress: {:?}",
self.send_header_state.write_state
);
self.send_header_state.pending_header = Some(header_bytes);
self.send_header_state.should_flush = should_flush;
self.send_header_state.write_state = HeaderWriteState::Idle;
self.send_header_state.timeout_duration = timeout;
self.send_header_state.timeout_fut = None;
}
pub async fn write_current_header_task<S>(&mut self, stream: &mut S) -> Result<usize>
where
S: AsyncWrite + Unpin + Send,
{
std::future::poll_fn(|cx| self.poll_write_current_header_task(cx, Pin::new(stream))).await
}
fn poll_write_current_header_task<S>(
&mut self,
cx: &mut Context<'_>,
stream: Pin<&mut S>,
) -> Poll<Result<usize>>
where
S: AsyncWrite + Unpin + Send,
{
if matches!(
self.send_header_state.write_state,
HeaderWriteState::TimedOut
) {
return Poll::Ready(Error::e_explain(
WriteTimedout,
"header write task previously timed out",
));
}
match self.poll_do_write_header_and_flush(cx, stream) {
Poll::Ready(Ok(size)) => {
if matches!(self.send_header_state.write_state, HeaderWriteState::Done) {
self.send_header_state.timeout_fut = None;
}
return Poll::Ready(Ok(size));
}
Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
Poll::Pending => {
}
}
if let Some(duration) = self.send_header_state.timeout_duration {
let timeout = self.send_header_state.timeout_fut.get_or_insert_with(|| {
Box::pin(pingora_timeout::sleep(duration))
as std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send + Sync>>
});
if timeout.as_mut().poll(cx).is_ready() {
self.send_header_state.write_state = HeaderWriteState::TimedOut;
self.send_header_state.timeout_fut = None;
return Poll::Ready(Error::e_explain(
WriteTimedout,
"writing header task timed out",
));
}
}
Poll::Pending
}
fn poll_do_write_header_and_flush<S>(
&mut self,
cx: &mut Context<'_>,
mut stream: Pin<&mut S>,
) -> Poll<Result<usize>>
where
S: AsyncWrite + Unpin + Send,
{
if matches!(self.send_header_state.write_state, HeaderWriteState::Idle) {
if let Some(header_bytes) = self.send_header_state.pending_header.take() {
let size = header_bytes.len();
self.send_header_state.write_state = HeaderWriteState::Writing(size, header_bytes);
} else {
self.send_header_state.write_state = HeaderWriteState::Done;
return Poll::Ready(Ok(0));
}
}
if let HeaderWriteState::Writing(original_size, ref mut buf) =
self.send_header_state.write_state
{
let size = original_size;
ready!(pin!(stream.as_mut().get_mut().write_all_buf(buf)).poll(cx))
.map_err(|e| Error::because(WriteError, "writing response header", e))?;
if self.send_header_state.should_flush {
self.send_header_state.write_state = HeaderWriteState::Flushing(size);
} else {
self.send_header_state.write_state = HeaderWriteState::Done;
return Poll::Ready(Ok(size));
}
}
match self.send_header_state.write_state {
HeaderWriteState::Flushing(size) => {
ready!(stream.as_mut().poll_flush(cx))
.map_err(|e| Error::because(WriteError, "flushing response header", e))?;
self.send_header_state.write_state = HeaderWriteState::Done;
Poll::Ready(Ok(size))
}
HeaderWriteState::Done => Poll::Ready(Ok(0)),
HeaderWriteState::TimedOut => Poll::Ready(Error::e_explain(
WriteTimedout,
"header write task previously timed out",
)),
HeaderWriteState::Idle => {
unreachable!("Idle state should have been handled above")
}
HeaderWriteState::Writing(..) => {
unreachable!("Writing state should have been handled above")
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::protocols::http::v1::test_util::FlushTrackingMock;
use tokio_test::io::Builder;
fn init_log() {
let _ = env_logger::builder().is_test(true).try_init();
}
#[tokio::test]
async fn test_simple_header_write_no_flush() {
init_log();
let header_data = b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\n";
let mock_io = Builder::new().write(header_data).build();
let (mut flush_mock, flush_count) = FlushTrackingMock::new(mock_io);
let mut header_writer = HeaderWriter::new();
header_writer.send_header_task(Bytes::from_static(header_data), false, None);
let result = header_writer
.write_current_header_task(&mut flush_mock)
.await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), header_data.len());
assert_eq!(
FlushTrackingMock::flush_count(&flush_count),
0,
"should_flush=false should not flush"
);
}
#[tokio::test]
async fn test_header_write_with_flush() {
init_log();
let header_data = b"HTTP/1.1 200 OK\r\n\r\n";
let mock_io = Builder::new().write(header_data).build();
let (mut flush_mock, flush_count) = FlushTrackingMock::new(mock_io);
let mut header_writer = HeaderWriter::new();
header_writer.send_header_task(Bytes::from_static(header_data), true, None);
let result = header_writer
.write_current_header_task(&mut flush_mock)
.await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), header_data.len());
assert_eq!(
FlushTrackingMock::flush_count(&flush_count),
1,
"should_flush=true should flush exactly once"
);
}
#[tokio::test(start_paused = true)]
async fn test_cancel_safe_header_write() {
init_log();
let header_data = b"HTTP/1.1 200 OK\r\nServer: pingora\r\n\r\n";
let mut mock_io = Builder::new()
.wait(std::time::Duration::from_millis(100))
.write(header_data)
.build();
let mut header_writer = HeaderWriter::new();
header_writer.send_header_task(Bytes::from_static(header_data), false, None);
let mut cancel_count = 0;
loop {
if !header_writer.has_pending_header_task() {
break;
}
tokio::select! {
_ = tokio::time::sleep(std::time::Duration::from_millis(10)) => {
cancel_count += 1;
}
result = header_writer.write_current_header_task(&mut mock_io) => {
assert!(result.is_ok());
assert_eq!(result.unwrap(), header_data.len());
break;
}
}
}
assert!(cancel_count > 0, "Should have cancelled at least once");
}
#[tokio::test]
async fn test_header_write_timeout() {
init_log();
let header_data = b"HTTP/1.1 200 OK\r\n\r\n";
let mut mock_io = Builder::new()
.wait(std::time::Duration::from_secs(1000))
.build();
let mut header_writer = HeaderWriter::new();
header_writer.send_header_task(
Bytes::from_static(header_data),
false,
Some(std::time::Duration::from_millis(50)),
);
let result = header_writer.write_current_header_task(&mut mock_io).await;
assert!(result.is_err());
assert_eq!(result.unwrap_err().etype(), &WriteTimedout);
}
#[tokio::test]
async fn test_header_write_timeout_persists() {
init_log();
let header_data = b"HTTP/1.1 200 OK\r\n\r\n";
let mut mock_io = Builder::new()
.wait(std::time::Duration::from_millis(200))
.build();
let mut header_writer = HeaderWriter::new();
header_writer.send_header_task(
Bytes::from_static(header_data),
false,
Some(std::time::Duration::from_millis(100)),
);
let mut attempts = 0;
let mut timedout = false;
loop {
if !header_writer.has_pending_header_task() {
break;
}
attempts += 1;
tokio::select! {
_ = tokio::time::sleep(std::time::Duration::from_millis(10)) => {
continue;
}
result = header_writer.write_current_header_task(&mut mock_io) => {
match result {
Ok(_) => break,
Err(e) if e.etype() == &WriteTimedout => {
timedout = true;
break;
}
Err(e) => panic!("Unexpected error: {:?}", e),
}
}
}
}
assert!(timedout, "Timeout should have fired");
assert!(attempts >= 5, "Should have had multiple attempts");
}
}