use std::cmp::Ordering;
use std::collections::BinaryHeap;
use std::path::PathBuf;
use std::time::{Duration, Instant};
use jiff::Timestamp;
use tokio::sync::mpsc;
use crate::errors::TaleError;
const TIMESTAMP_FIELDS: [&str; 3] = ["timestamp", "time", "ts"];
#[derive(Debug, Clone)]
pub struct BatchedLine {
pub content: String,
pub parsed_json: Option<serde_json::Value>,
pub _source_file: PathBuf,
pub timestamp: Option<Timestamp>,
pub received_at: Instant,
pub _line_number: u64,
}
impl BatchedLine {
pub fn new(content: String, source_file: PathBuf, line_number: u64) -> Self {
Self {
content,
parsed_json: None,
_source_file: source_file,
timestamp: None,
received_at: Instant::now(),
_line_number: line_number,
}
}
pub fn parse(&mut self) -> Result<(), TaleError> {
match serde_json::from_str::<serde_json::Value>(&self.content) {
Ok(json_value) => {
self.timestamp = self.extract_timestamp(&json_value);
self.parsed_json = Some(json_value);
}
Err(_) => {
self.parsed_json = None;
self.timestamp = None;
}
}
Ok(())
}
fn extract_timestamp(&self, json: &serde_json::Value) -> Option<Timestamp> {
if let Some(obj) = json.as_object() {
for field in &TIMESTAMP_FIELDS {
if let Some(ts_value) = obj.get(*field)
&& let Some(ts_str) = ts_value.as_str()
&& let Ok(timestamp) = ts_str.parse::<Timestamp>()
{
return Some(timestamp);
}
}
}
None
}
pub fn sort_key(&self) -> SortKey {
if let Some(ts) = &self.timestamp {
SortKey::Timestamp(*ts)
} else {
SortKey::ReceivedAt(self.received_at)
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SortKey {
Timestamp(Timestamp),
ReceivedAt(Instant),
}
impl PartialOrd for SortKey {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for SortKey {
fn cmp(&self, other: &Self) -> Ordering {
match (self, other) {
(SortKey::Timestamp(a), SortKey::Timestamp(b)) => a.cmp(b),
(SortKey::ReceivedAt(a), SortKey::ReceivedAt(b)) => a.cmp(b),
(SortKey::Timestamp(_), SortKey::ReceivedAt(_)) => Ordering::Less,
(SortKey::ReceivedAt(_), SortKey::Timestamp(_)) => Ordering::Greater,
}
}
}
#[derive(Debug)]
struct MinHeapLine(BatchedLine);
impl PartialEq for MinHeapLine {
fn eq(&self, other: &Self) -> bool {
self.0.sort_key() == other.0.sort_key()
}
}
impl Eq for MinHeapLine {}
impl PartialOrd for MinHeapLine {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for MinHeapLine {
fn cmp(&self, other: &Self) -> Ordering {
other.0.sort_key().cmp(&self.0.sort_key())
}
}
#[derive(Debug, Clone)]
pub struct BatchConfig {
pub batch_window: Duration,
pub max_batch_size: usize,
pub _max_buffer_memory: usize,
}
impl Default for BatchConfig {
fn default() -> Self {
Self {
batch_window: Duration::from_millis(250),
max_batch_size: 200,
_max_buffer_memory: 2 * 1024 * 1024, }
}
}
pub struct BatchProcessor {
config: BatchConfig,
pending_lines: BinaryHeap<MinHeapLine>,
receiver: Option<mpsc::UnboundedReceiver<BatchedLine>>,
sender: Option<mpsc::UnboundedSender<Vec<BatchedLine>>>,
window_start: Option<Instant>,
}
impl BatchProcessor {
pub fn new(config: BatchConfig) -> Self {
Self {
config,
pending_lines: BinaryHeap::new(),
receiver: None,
sender: None,
window_start: None,
}
}
pub async fn start(
&mut self,
) -> Result<
(
mpsc::UnboundedSender<BatchedLine>,
mpsc::UnboundedReceiver<Vec<BatchedLine>>,
),
TaleError,
> {
let (line_sender, line_receiver) = mpsc::unbounded_channel();
let (batch_sender, batch_receiver) = mpsc::unbounded_channel();
let mut processor = BatchProcessor {
config: self.config.clone(),
pending_lines: BinaryHeap::new(),
receiver: Some(line_receiver),
sender: Some(batch_sender),
window_start: None,
};
tokio::spawn(async move {
if let Err(e) = processor.process_loop().await {
eprintln!("Batch processor error: {e}");
}
});
Ok((line_sender, batch_receiver))
}
async fn process_loop(&mut self) -> Result<(), TaleError> {
let Some(line_receiver) = self.receiver.take() else {
return Err(TaleError::LineReceiver);
};
let Some(batch_sender) = self.sender.take() else {
return Err(TaleError::BatchSender);
};
let mut line_receiver = line_receiver;
let batch_timeout = tokio::time::interval(self.config.batch_window);
tokio::pin!(batch_timeout);
loop {
tokio::select! {
line_opt = line_receiver.recv() => {
match line_opt {
Some(mut line) => {
if let Err(_e) = line.parse() {
}
self.pending_lines.push(MinHeapLine(line));
if self.window_start.is_none() {
self.window_start = Some(Instant::now());
}
if self.pending_lines.len() >= self.config.max_batch_size {
self.emit_batch(&batch_sender)?;
}
}
None => {
self.emit_all_pending(&batch_sender)?;
break;
}
}
}
_ = batch_timeout.tick() => {
if !self.pending_lines.is_empty() {
self.emit_batch(&batch_sender)?;
}
}
}
}
Ok(())
}
fn emit_batch(&mut self, batch_sender: &mpsc::UnboundedSender<Vec<BatchedLine>>) -> Result<(), TaleError> {
if self.pending_lines.is_empty() {
return Ok(());
}
let mut sorted_lines = Vec::new();
while let Some(min_line) = self.pending_lines.pop() {
sorted_lines.push(min_line.0);
}
self.window_start = None;
match batch_sender.send(sorted_lines) {
Ok(_) => Ok(()),
Err(_) => Err(TaleError::BatchedLineVecSender),
}
}
fn emit_all_pending(&mut self, batch_sender: &mpsc::UnboundedSender<Vec<BatchedLine>>) -> Result<(), TaleError> {
self.emit_batch(batch_sender)
}
}
pub fn batched_with_config(config: BatchConfig) -> BatchProcessor {
BatchProcessor::new(config)
}