use bytes::Bytes;
use crossbeam_queue::ArrayQueue;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::mpsc::Sender;
use webui_handler::{FlushWriter, HandlerError, ResponseWriter, Result};
pub struct ChunkPool {
queue: ArrayQueue<Vec<u8>>,
chunk_size: usize,
}
impl ChunkPool {
#[must_use]
pub fn new(max_pool: usize, chunk_size: usize) -> Self {
Self {
queue: ArrayQueue::new(max_pool.max(1)),
chunk_size,
}
}
fn acquire(&self) -> Vec<u8> {
match self.queue.pop() {
Some(mut buf) => {
debug_assert!(
buf.is_empty(),
"ChunkPool invariant violation: pool returned non-empty buffer"
);
if buf.capacity() < self.chunk_size {
buf.reserve(self.chunk_size - buf.capacity());
}
buf
}
None => Vec::with_capacity(self.chunk_size),
}
}
fn release(&self, mut buf: Vec<u8>) {
buf.clear();
let _ = self.queue.push(buf);
}
#[must_use]
pub fn idle_count(&self) -> usize {
self.queue.len()
}
#[must_use]
pub fn capacity(&self) -> usize {
self.queue.capacity()
}
}
struct PooledChunk {
buf: Option<Vec<u8>>,
pool: Arc<ChunkPool>,
}
impl PooledChunk {
#[inline]
fn new(buf: Vec<u8>, pool: Arc<ChunkPool>) -> Self {
Self {
buf: Some(buf),
pool,
}
}
}
impl AsRef<[u8]> for PooledChunk {
#[inline]
fn as_ref(&self) -> &[u8] {
match self.buf.as_deref() {
Some(slice) => slice,
None => &[],
}
}
}
impl Drop for PooledChunk {
fn drop(&mut self) {
if let Some(buf) = self.buf.take() {
self.pool.release(buf);
}
}
}
pub struct StreamingWriter {
tx: Sender<Bytes>,
buf: Vec<u8>,
chunk_target: usize,
flush_timeout: Option<Duration>,
terminated: Option<TerminationCause>,
pool: Option<Arc<ChunkPool>>,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
enum TerminationCause {
Disconnected,
Timeout,
}
impl From<TerminationCause> for HandlerError {
fn from(cause: TerminationCause) -> Self {
match cause {
TerminationCause::Disconnected => HandlerError::ClientDisconnected,
TerminationCause::Timeout => HandlerError::StreamTimeout,
}
}
}
impl StreamingWriter {
pub const CHUNK_TARGET: usize = 4 * 1024;
pub const DEFAULT_CHANNEL_CAPACITY: usize = 4;
const MIN_CHUNK_TARGET: usize = 64;
const BUF_HEADROOM: usize = 1024;
#[must_use]
pub fn new(tx: Sender<Bytes>) -> Self {
Self {
tx,
buf: Vec::with_capacity(Self::CHUNK_TARGET + Self::BUF_HEADROOM),
chunk_target: Self::CHUNK_TARGET,
flush_timeout: None,
terminated: None,
pool: None,
}
}
#[must_use]
pub fn new_pooled(tx: Sender<Bytes>, pool: Arc<ChunkPool>) -> Self {
let buf = pool.acquire();
Self {
tx,
buf,
chunk_target: Self::CHUNK_TARGET,
flush_timeout: None,
terminated: None,
pool: Some(pool),
}
}
#[must_use]
pub fn with_chunk_size(mut self, bytes: usize) -> Self {
let target = bytes.max(Self::MIN_CHUNK_TARGET);
self.chunk_target = target;
let old = std::mem::replace(
&mut self.buf,
Vec::with_capacity(target + Self::BUF_HEADROOM),
);
if let Some(pool) = self.pool.as_ref() {
pool.release(old);
}
self
}
#[must_use]
pub fn with_flush_timeout(mut self, timeout: Duration) -> Self {
self.flush_timeout = Some(timeout);
self
}
fn flush_buf(&mut self) -> Result<()> {
if self.buf.is_empty() {
return Ok(());
}
if let Some(cause) = self.terminated {
return Err(cause.into());
}
let chunk = std::mem::take(&mut self.buf);
self.buf = match self.pool.as_ref() {
Some(pool) => {
let mut next = pool.acquire();
let want = self.chunk_target + Self::BUF_HEADROOM;
if next.capacity() < want {
next.reserve(want - next.capacity());
}
next
}
None => Vec::with_capacity(self.chunk_target + Self::BUF_HEADROOM),
};
let payload = match self.pool.as_ref() {
Some(pool) => Bytes::from_owner(PooledChunk::new(chunk, Arc::clone(pool))),
None => Bytes::from(chunk),
};
let outcome = send_with_optional_timeout(&self.tx, payload, self.flush_timeout);
match outcome {
SendOutcome::Ok => Ok(()),
SendOutcome::Disconnected => {
self.terminated = Some(TerminationCause::Disconnected);
Err(HandlerError::ClientDisconnected)
}
SendOutcome::TimedOut => {
self.terminated = Some(TerminationCause::Timeout);
Err(HandlerError::StreamTimeout)
}
}
}
#[must_use]
pub fn is_terminated(&self) -> bool {
self.terminated.is_some()
}
}
impl Drop for StreamingWriter {
fn drop(&mut self) {
if let Some(pool) = self.pool.as_ref() {
let buf = std::mem::take(&mut self.buf);
if buf.capacity() > 0 {
pool.release(buf);
}
}
}
}
enum SendOutcome {
Ok,
Disconnected,
TimedOut,
}
fn send_with_optional_timeout(
tx: &Sender<Bytes>,
payload: Bytes,
timeout: Option<Duration>,
) -> SendOutcome {
let Some(deadline) = timeout else {
return match tx.blocking_send(payload) {
Ok(()) => SendOutcome::Ok,
Err(_) => SendOutcome::Disconnected,
};
};
if let Ok(handle) = tokio::runtime::Handle::try_current() {
let timed =
handle.block_on(async { tokio::time::timeout(deadline, tx.send(payload)).await });
return match timed {
Ok(Ok(())) => SendOutcome::Ok,
Ok(Err(_)) => SendOutcome::Disconnected,
Err(_) => SendOutcome::TimedOut,
};
}
no_runtime_timeout_warn_once();
runtime_free_send(tx, payload, deadline)
}
fn no_runtime_timeout_warn_once() {
use std::sync::atomic::{AtomicBool, Ordering};
static WARNED: AtomicBool = AtomicBool::new(false);
if !WARNED.swap(true, Ordering::Relaxed) {
log::warn!(
"StreamingWriter::with_flush_timeout was set, but no tokio runtime is in TLS. \
Falling back to a runtime-free poll loop (slow-loris bound is preserved but \
with a small wakeup overshoot). Wire the writer from `spawn_blocking` to use \
the precise tokio path."
);
}
}
fn runtime_free_send(tx: &Sender<Bytes>, payload: Bytes, deadline: Duration) -> SendOutcome {
use tokio::sync::mpsc::error::TrySendError;
let start = Instant::now();
let mut payload = payload;
let mut interval = Duration::from_millis(1);
let backoff_after = Duration::from_secs(1);
loop {
match tx.try_send(payload) {
Ok(()) => return SendOutcome::Ok,
Err(TrySendError::Closed(_)) => return SendOutcome::Disconnected,
Err(TrySendError::Full(returned)) => {
if start.elapsed() >= deadline {
return SendOutcome::TimedOut;
}
std::thread::sleep(interval);
if start.elapsed() >= backoff_after && interval < Duration::from_millis(50) {
interval = Duration::from_millis(50);
}
payload = returned;
}
}
}
}
impl ResponseWriter for StreamingWriter {
fn write(&mut self, content: &str) -> Result<()> {
if let Some(cause) = self.terminated {
return Err(cause.into());
}
self.buf.extend_from_slice(content.as_bytes());
if self.buf.len() >= self.chunk_target {
self.flush_buf()?;
}
Ok(())
}
fn write_attribute(&mut self, name: &str, value: &str) -> Result<()> {
if let Some(cause) = self.terminated {
return Err(cause.into());
}
webui_handler::append_attribute_to_bytes(&mut self.buf, name, value);
if self.buf.len() >= self.chunk_target {
self.flush_buf()?;
}
Ok(())
}
fn write_boolean_attribute(&mut self, name: &str) -> Result<()> {
if let Some(cause) = self.terminated {
return Err(cause.into());
}
webui_handler::append_boolean_attribute_to_bytes(&mut self.buf, name);
if self.buf.len() >= self.chunk_target {
self.flush_buf()?;
}
Ok(())
}
fn end(&mut self) -> Result<()> {
if self.terminated.is_some() {
return Ok(());
}
self.flush_buf()
}
}
impl FlushWriter for StreamingWriter {
fn flush(&mut self) -> Result<()> {
self.flush_buf()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn drain(mut rx: tokio::sync::mpsc::Receiver<Bytes>) -> String {
let mut buf = Vec::new();
while let Ok(chunk) = rx.try_recv() {
buf.extend_from_slice(&chunk);
}
String::from_utf8(buf).expect("valid utf-8")
}
#[test]
fn streaming_writer_coalesces_small_writes() {
let (tx, rx) = tokio::sync::mpsc::channel::<Bytes>(8);
let mut w = StreamingWriter::new(tx);
for _ in 0..10 {
ResponseWriter::write(&mut w, "abc").unwrap();
}
ResponseWriter::end(&mut w).unwrap();
drop(w);
assert_eq!(drain(rx), "abc".repeat(10));
}
#[test]
fn streaming_writer_flushes_at_chunk_boundary() {
let (tx, mut rx) = tokio::sync::mpsc::channel::<Bytes>(8);
let mut w = StreamingWriter::new(tx);
let big = "x".repeat(StreamingWriter::CHUNK_TARGET);
ResponseWriter::write(&mut w, &big).unwrap();
let first = rx.try_recv().expect("first chunk should be available");
assert_eq!(first.len(), StreamingWriter::CHUNK_TARGET);
ResponseWriter::end(&mut w).unwrap();
}
#[test]
fn streaming_writer_explicit_flush_sends_partial_chunk() {
let (tx, mut rx) = tokio::sync::mpsc::channel::<Bytes>(8);
let mut writer = StreamingWriter::new(tx);
ResponseWriter::write(&mut writer, "boundary").unwrap();
assert!(rx.try_recv().is_err());
FlushWriter::flush(&mut writer).unwrap();
assert_eq!(
rx.try_recv().expect("explicitly flushed chunk"),
Bytes::from_static(b"boundary")
);
}
#[test]
fn streaming_writer_returns_typed_error_after_disconnect() {
let (tx, rx) = tokio::sync::mpsc::channel::<Bytes>(1);
let mut w = StreamingWriter::new(tx);
drop(rx);
ResponseWriter::write(&mut w, "hi").unwrap();
let big = "x".repeat(StreamingWriter::CHUNK_TARGET);
let result = ResponseWriter::write(&mut w, &big);
assert!(matches!(result, Err(HandlerError::ClientDisconnected)));
assert!(w.is_terminated());
let result2 = ResponseWriter::write(&mut w, "more");
assert!(matches!(result2, Err(HandlerError::ClientDisconnected)));
}
#[test]
fn streaming_writer_end_after_disconnect_succeeds() {
let (tx, rx) = tokio::sync::mpsc::channel::<Bytes>(1);
let mut w = StreamingWriter::new(tx);
drop(rx);
let big = "x".repeat(StreamingWriter::CHUNK_TARGET);
let _ = ResponseWriter::write(&mut w, &big);
assert!(w.is_terminated());
ResponseWriter::end(&mut w).unwrap();
}
#[test]
fn streaming_writer_end_surfaces_first_flush_error() {
let (tx, rx) = tokio::sync::mpsc::channel::<Bytes>(1);
let mut w = StreamingWriter::new(tx);
drop(rx);
ResponseWriter::write(&mut w, "small").unwrap();
assert!(!w.is_terminated(), "no automatic flush yet");
let result = ResponseWriter::end(&mut w);
assert!(
matches!(result, Err(HandlerError::ClientDisconnected)),
"end() must surface ClientDisconnected from final flush, got {result:?}"
);
assert!(w.is_terminated(), "writer must be marked terminated");
}
#[test]
fn streaming_writer_custom_chunk_size() {
let (tx, mut rx) = tokio::sync::mpsc::channel::<Bytes>(8);
let mut w = StreamingWriter::new(tx).with_chunk_size(128);
ResponseWriter::write(&mut w, &"x".repeat(127)).unwrap();
assert!(rx.try_recv().is_err(), "below threshold, no flush yet");
ResponseWriter::write(&mut w, "x").unwrap();
let first = rx.try_recv().expect("chunk should flush at 128 bytes");
assert_eq!(first.len(), 128);
}
#[test]
fn streaming_writer_min_chunk_size_clamp() {
let (tx, _rx) = tokio::sync::mpsc::channel::<Bytes>(8);
let w = StreamingWriter::new(tx).with_chunk_size(1);
assert_eq!(w.chunk_target, StreamingWriter::MIN_CHUNK_TARGET);
}
#[test]
fn streaming_writer_flush_timeout_fires_without_runtime() {
let (tx, _rx) = tokio::sync::mpsc::channel::<Bytes>(1);
let mut w = StreamingWriter::new(tx)
.with_chunk_size(64)
.with_flush_timeout(Duration::from_millis(150));
ResponseWriter::write(&mut w, &"x".repeat(64)).unwrap();
let start = Instant::now();
let result = ResponseWriter::write(&mut w, &"y".repeat(64));
let elapsed = start.elapsed();
assert!(
matches!(result, Err(HandlerError::StreamTimeout)),
"expected Err(StreamTimeout), got {result:?}"
);
assert!(
elapsed >= Duration::from_millis(150),
"must wait at least the deadline; elapsed={elapsed:?}"
);
assert!(
elapsed < Duration::from_millis(1500),
"must not block much past the deadline; elapsed={elapsed:?}"
);
assert!(w.is_terminated(), "writer must be marked terminated");
let start = Instant::now();
let result2 = ResponseWriter::write(&mut w, "more");
assert!(matches!(result2, Err(HandlerError::StreamTimeout)));
assert!(
start.elapsed() < Duration::from_millis(50),
"subsequent writes must short-circuit; elapsed={:?}",
start.elapsed()
);
}
#[test]
fn pool_round_trip() {
let pool = ChunkPool::new(4, 1024);
let buf = pool.acquire();
assert!(buf.capacity() >= 1024);
assert_eq!(buf.len(), 0);
assert_eq!(pool.idle_count(), 0);
pool.release(buf);
assert_eq!(pool.idle_count(), 1);
let buf2 = pool.acquire();
assert!(buf2.capacity() >= 1024);
assert_eq!(pool.idle_count(), 0);
}
#[test]
fn pool_clears_dirty_buffer_on_acquire() {
let pool = ChunkPool::new(2, 16);
let mut dirty = Vec::with_capacity(64);
dirty.extend_from_slice(b"leftover content");
pool.release(dirty);
let acquired = pool.acquire();
assert_eq!(acquired.len(), 0, "acquired buffer must be empty");
}
#[test]
fn pool_full_drops_excess() {
let pool = ChunkPool::new(2, 8);
pool.release(Vec::with_capacity(8));
pool.release(Vec::with_capacity(8));
assert_eq!(pool.idle_count(), 2);
pool.release(Vec::with_capacity(8));
assert_eq!(pool.idle_count(), 2, "pool must not grow beyond capacity");
}
#[test]
fn pooled_chunk_drop_returns_to_pool() {
let pool = Arc::new(ChunkPool::new(4, 256));
assert_eq!(pool.idle_count(), 0);
let buf = pool.acquire();
assert_eq!(pool.idle_count(), 0);
let payload = Bytes::from_owner(PooledChunk::new(buf, Arc::clone(&pool)));
assert_eq!(pool.idle_count(), 0);
drop(payload);
assert_eq!(pool.idle_count(), 1);
}
#[test]
fn pooled_chunk_returns_after_last_clone_drop() {
let pool = Arc::new(ChunkPool::new(4, 256));
let buf = pool.acquire();
let original = Bytes::from_owner(PooledChunk::new(buf, Arc::clone(&pool)));
let clone1 = original.clone();
let clone2 = original.clone();
assert_eq!(pool.idle_count(), 0);
drop(original);
assert_eq!(pool.idle_count(), 0, "still 2 refs alive");
drop(clone1);
assert_eq!(pool.idle_count(), 0, "still 1 ref alive");
drop(clone2);
assert_eq!(pool.idle_count(), 1, "last ref dropped, buffer returned");
}
#[test]
fn streaming_writer_pooled_recycles_buffers() {
let pool = Arc::new(ChunkPool::new(8, StreamingWriter::CHUNK_TARGET));
{
let (tx, rx) = tokio::sync::mpsc::channel::<Bytes>(8);
let mut w = StreamingWriter::new_pooled(tx, Arc::clone(&pool));
for _ in 0..3 {
ResponseWriter::write(&mut w, &"x".repeat(StreamingWriter::CHUNK_TARGET)).unwrap();
}
ResponseWriter::end(&mut w).unwrap();
drop(w);
let _ = drain(rx);
}
let after_first = pool.idle_count();
assert!(
after_first >= 3,
"after first render, pool should have ≥3 buffers; got {after_first}"
);
let before_second = pool.idle_count();
{
let (tx, rx) = tokio::sync::mpsc::channel::<Bytes>(8);
let mut w = StreamingWriter::new_pooled(tx, Arc::clone(&pool));
for _ in 0..3 {
ResponseWriter::write(&mut w, &"x".repeat(StreamingWriter::CHUNK_TARGET)).unwrap();
}
ResponseWriter::end(&mut w).unwrap();
drop(w);
let _ = drain(rx);
}
let after_second = pool.idle_count();
assert!(
after_second >= before_second.saturating_sub(1),
"pool should not shrink across renders: before={before_second} after={after_second}"
);
}
#[test]
fn pooled_chunk_cross_thread_drop() {
let pool = Arc::new(ChunkPool::new(4, 128));
let buf = pool.acquire();
let payload = Bytes::from_owner(PooledChunk::new(buf, Arc::clone(&pool)));
let pool_for_thread = Arc::clone(&pool);
let h = std::thread::spawn(move || {
drop(payload);
assert_eq!(pool_for_thread.idle_count(), 1);
});
h.join().unwrap();
assert_eq!(pool.idle_count(), 1);
}
}