use super::client::open;
use super::http::{Request, Response, StreamSink};
use crate::event_log::EventLog;
use crate::stop::StopToken;
use serde_json::{json, Value};
use std::io::{self, BufRead, Read};
use std::sync::Arc;
use std::time::Duration;
pub const KEEPALIVE: Duration = Duration::from_secs(15);
const POLL_WAIT: Duration = Duration::from_secs(2);
fn request_cursor(request: &Request) -> Option<u64> {
request
.header("last-event-id")
.and_then(|v| v.trim().parse::<u64>().ok())
.or_else(|| {
request
.query_pairs()
.into_iter()
.find(|(k, _)| k == "after")
.and_then(|(_, v)| v.trim().parse::<u64>().ok())
})
}
pub fn replay_response(log: Arc<EventLog>, request: &Request, topics: Vec<String>) -> Response {
let cursor = request_cursor(request);
Response::event_stream(Box::new(move |sink: &mut dyn StreamSink| {
let mut after = match cursor {
Some(after) => after,
None => log.poll(u64::MAX, &topics, Duration::ZERO).next,
};
let mut quiet = Duration::ZERO;
loop {
if sink.stopping() {
return;
}
let polled = log.poll(after, &topics, POLL_WAIT);
if polled.dropped {
let frame = format!(
"event: resync\ndata: {}\n\n",
json!({ "next": polled.next })
);
if sink.send(frame.as_bytes()).is_err() {
return;
}
after = polled.next;
continue;
}
if polled.events.is_empty() {
after = polled.next.max(after);
quiet += POLL_WAIT;
if quiet >= KEEPALIVE {
quiet = Duration::ZERO;
if sink.send(b": keepalive\n\n").is_err() {
return;
}
}
continue;
}
quiet = Duration::ZERO;
for event in &polled.events {
let data = serde_json::to_string(&*event.payload).unwrap_or_else(|_| "null".into());
let frame = format!(
"id: {}\nevent: {}\ndata: {}\n\n",
event.seq, event.topic, data
);
if sink.send(frame.as_bytes()).is_err() {
return;
}
}
after = polled.next;
}
}))
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum ReplayEvent {
Connected,
Event {
seq: u64,
topic: String,
data: Value,
},
Resync { next: Option<u64> },
Disconnected { error: String, retry_in: Duration },
}
#[derive(Debug, Clone)]
pub struct ReplayConfig {
pub port: u16,
pub token: String,
pub path: String,
pub cursor: Option<u64>,
pub backoff_initial: Duration,
pub backoff_max: Duration,
pub idle_timeout: Duration,
}
impl ReplayConfig {
pub fn new(port: u16, token: impl Into<String>, path: impl Into<String>) -> Self {
Self {
port,
token: token.into(),
path: path.into(),
cursor: None,
backoff_initial: Duration::from_millis(250),
backoff_max: Duration::from_secs(10),
idle_timeout: Duration::from_secs(45),
}
}
#[must_use]
pub fn cursor(mut self, cursor: Option<u64>) -> Self {
self.cursor = cursor;
self
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ReplayCursor {
last: Option<u64>,
}
impl ReplayCursor {
pub fn new(last: Option<u64>) -> Self {
Self { last }
}
pub fn last(&self) -> Option<u64> {
self.last
}
pub fn admit(&mut self, seq: u64) -> bool {
if self.last.is_some_and(|last| seq <= last) {
return false;
}
self.last = Some(seq);
true
}
}
pub fn run_replay_stream(
config: &ReplayConfig,
stop: &StopToken,
mut on_event: impl FnMut(ReplayEvent),
) -> Option<u64> {
let mut cursor = ReplayCursor::new(config.cursor);
let mut failures = 0u32;
while !stop.is_stopped() {
let outcome = stream_once(config, stop, &mut cursor, &mut on_event, &mut failures);
if stop.is_stopped() {
break;
}
let error = match outcome {
Ok(()) => "the stream ended".to_string(),
Err(e) => e.to_string(),
};
let delay = config
.backoff_initial
.saturating_mul(1u32 << failures.min(10))
.min(config.backoff_max)
.max(Duration::from_millis(1));
failures = failures.saturating_add(1);
on_event(ReplayEvent::Disconnected {
error,
retry_in: delay,
});
if stop.wait(delay).is_some() {
break;
}
}
cursor.last()
}
fn stream_once(
config: &ReplayConfig,
stop: &StopToken,
cursor: &mut ReplayCursor,
on_event: &mut impl FnMut(ReplayEvent),
failures: &mut u32,
) -> io::Result<()> {
let mut extra: Vec<(&str, String)> = vec![("Accept", "text/event-stream".to_string())];
if let Some(last) = cursor.last() {
extra.push(("Last-Event-ID", last.to_string()));
}
let mut response = open(
config.port,
"GET",
&config.path,
Some(&config.token),
&extra,
None,
config.idle_timeout,
)?;
if response.status != 200 {
return Err(io::Error::other(format!(
"the daemon answered {}",
response.status
)));
}
*failures = 0;
on_event(ReplayEvent::Connected);
let chunked = response.chunked();
let mut decoder = Frame::default();
let mut pending = String::new();
loop {
if stop.is_stopped() {
return Ok(());
}
let line = if chunked {
match next_chunk_line(&mut response.reader, &mut pending)? {
Some(line) => line,
None => return Ok(()),
}
} else {
let mut raw = String::new();
if response.reader.read_line(&mut raw)? == 0 {
return Ok(());
}
raw.trim_end_matches(['\r', '\n']).to_string()
};
if let Some(frame) = decoder.push_line(&line) {
deliver(frame, cursor, on_event);
}
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
struct Done {
id: Option<u64>,
event: String,
data: String,
}
#[derive(Default)]
struct Frame {
current: Done,
has_data: bool,
}
impl Frame {
fn push_line(&mut self, line: &str) -> Option<Done> {
if line.is_empty() {
let frame = std::mem::take(&mut self.current);
let had = std::mem::replace(&mut self.has_data, false);
return had.then_some(frame);
}
if line.starts_with(':') {
return None;
}
let (field, value) = match line.split_once(':') {
Some((f, v)) => (f, v.strip_prefix(' ').unwrap_or(v)),
None => (line, ""),
};
match field {
"id" => self.current.id = value.trim().parse::<u64>().ok(),
"event" => self.current.event = value.to_string(),
"data" => {
if self.has_data {
self.current.data.push('\n');
}
self.current.data.push_str(value);
self.has_data = true;
}
_ => {}
}
None
}
}
fn deliver(frame: Done, cursor: &mut ReplayCursor, on_event: &mut impl FnMut(ReplayEvent)) {
let data: Value = serde_json::from_str(&frame.data).unwrap_or(Value::String(frame.data));
if frame.event == "resync" {
on_event(ReplayEvent::Resync {
next: data.get("next").and_then(Value::as_u64),
});
return;
}
let Some(seq) = frame.id else {
return;
};
if cursor.admit(seq) {
on_event(ReplayEvent::Event {
seq,
topic: frame.event,
data,
});
}
}
fn next_chunk_line(
reader: &mut std::io::BufReader<std::net::TcpStream>,
pending: &mut String,
) -> io::Result<Option<String>> {
loop {
if let Some(i) = pending.find('\n') {
let line: String = pending.drain(..=i).collect();
return Ok(Some(line.trim_end_matches(['\r', '\n']).to_string()));
}
let mut size_line = String::new();
if reader.read_line(&mut size_line)? == 0 {
return Ok(None);
}
let size_text = size_line.trim().split(';').next().unwrap_or("");
let size = usize::from_str_radix(size_text, 16)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "bad chunk size"))?;
if size == 0 {
return Ok(None);
}
if size > 8 * 1024 * 1024 || pending.len() + size > 8 * 1024 * 1024 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"stream chunk too large",
));
}
let mut data = vec![0u8; size];
reader.read_exact(&mut data)?;
let mut crlf = [0u8; 2];
reader.read_exact(&mut crlf)?;
pending.push_str(
&String::from_utf8(data)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "stream is not UTF-8"))?,
);
}
}