systemprompt_logging/layer/
writer.rs1use std::io::Write;
8use std::time::Duration;
9
10use systemprompt_traits::OwnedTask;
11use tokio::sync::mpsc;
12
13use crate::models::LogEntry;
14use crate::repository::LoggingRepository;
15
16const BUFFER_FLUSH_SIZE: usize = 100;
17const BUFFER_FLUSH_INTERVAL_SECS: u64 = 10;
18
19pub(super) enum LogCommand {
20 Entry(Box<LogEntry>),
21 FlushNow,
22 Shutdown,
23}
24
25#[derive(Debug, thiserror::Error)]
26pub enum LogWriterShutdownError {
27 #[error("the log writer had already stopped before the shutdown flush was requested")]
28 WriterGone,
29
30 #[error("the log writer task failed: {0}")]
31 Join(#[from] tokio::task::JoinError),
32}
33
34#[derive(Debug)]
35pub struct LogWriterHandle {
36 sender: mpsc::Sender<LogCommand>,
37 task: OwnedTask<()>,
38}
39
40impl LogWriterHandle {
41 pub(super) fn spawn(
42 repository: LoggingRepository,
43 sender: mpsc::Sender<LogCommand>,
44 receiver: mpsc::Receiver<LogCommand>,
45 ) -> Self {
46 Self {
47 sender,
48 task: OwnedTask::spawn("log_batch_writer", batch_writer(repository, receiver)),
49 }
50 }
51
52 pub async fn shutdown(self) -> Result<(), LogWriterShutdownError> {
53 let requested = self.sender.send(LogCommand::Shutdown).await;
54 self.task.join().await?;
55 requested.map_err(|_closed| LogWriterShutdownError::WriterGone)
56 }
57}
58
59async fn batch_writer(repository: LoggingRepository, mut receiver: mpsc::Receiver<LogCommand>) {
60 let mut buffer = Vec::with_capacity(BUFFER_FLUSH_SIZE);
61 let mut interval = tokio::time::interval(Duration::from_secs(BUFFER_FLUSH_INTERVAL_SECS));
62 let mut failed_total: u64 = 0;
63
64 loop {
65 tokio::select! {
66 command = receiver.recv() => match command {
67 Some(LogCommand::Entry(entry)) => {
68 buffer.push(*entry);
69 if buffer.len() >= BUFFER_FLUSH_SIZE {
70 flush(&repository, &mut buffer, &mut failed_total).await;
71 }
72 }
73 Some(LogCommand::FlushNow) => {
74 if !buffer.is_empty() {
75 flush(&repository, &mut buffer, &mut failed_total).await;
76 }
77 }
78 Some(LogCommand::Shutdown) | None => {
79 drain_queued(&mut receiver, &mut buffer);
80 if !buffer.is_empty() {
81 flush(&repository, &mut buffer, &mut failed_total).await;
82 }
83 return;
84 }
85 },
86 _ = interval.tick() => {
87 if !buffer.is_empty() {
88 flush(&repository, &mut buffer, &mut failed_total).await;
89 }
90 }
91 }
92 }
93}
94
95fn drain_queued(receiver: &mut mpsc::Receiver<LogCommand>, buffer: &mut Vec<LogEntry>) {
96 while let Ok(command) = receiver.try_recv() {
97 if let LogCommand::Entry(entry) = command {
98 buffer.push(*entry);
99 }
100 }
101}
102
103async fn flush(repository: &LoggingRepository, buffer: &mut Vec<LogEntry>, failed_total: &mut u64) {
104 if let Err(e) = repository.insert_batch(buffer).await {
105 let lost = u64::try_from(buffer.len()).unwrap_or(u64::MAX);
106 *failed_total = failed_total.saturating_add(lost);
107 let _stderr_written = writeln!(
110 std::io::stderr(),
111 "DATABASE LOG FLUSH FAILED ({lost} entries lost this flush, {failed_total} total lost since start): {e}"
112 )
113 .is_ok();
114 }
115 buffer.clear();
116}