use std::path::{Path, PathBuf};
use std::sync::Arc;
use redb::{Database, ReadableDatabase, ReadableTable, TableDefinition};
use tokio::sync::mpsc;
use crate::{Log, LogError, is_heartbeat};
const ASYNC_LOG_CHANNEL_CAPACITY: usize = 1024;
const INCOMING: TableDefinition<u64, (i64, &str, &str)> = TableDefinition::new("log_incoming");
const OUTGOING: TableDefinition<u64, (i64, &str, &str)> = TableDefinition::new("log_outgoing");
const EVENT: TableDefinition<u64, (i64, &str, &str)> = TableDefinition::new("log_event");
fn backend<E: std::fmt::Display>(e: E) -> LogError {
LogError::Io(e.to_string())
}
fn now_unix() -> i64 {
time::OffsetDateTime::now_utc().unix_timestamp()
}
enum Entry {
Message {
direction: &'static str,
text: String,
},
Event {
text: String,
},
}
#[derive(Debug, Clone)]
pub struct RedbLogConfig {
pub path: PathBuf,
pub include_heartbeats: bool,
pub session_id: String,
}
impl RedbLogConfig {
pub fn new(path: impl Into<PathBuf>) -> Self {
Self {
path: path.into(),
include_heartbeats: true,
session_id: "default".to_owned(),
}
}
}
pub struct RedbLog {
tx: std::sync::Mutex<Option<mpsc::Sender<Entry>>>,
task: std::sync::Mutex<Option<tokio::task::JoinHandle<()>>>,
include_heartbeats: bool,
}
fn next_key(
db: &Database,
table: TableDefinition<u64, (i64, &str, &str)>,
) -> Result<u64, LogError> {
let txn = db.begin_read().map_err(backend)?;
let t = txn.open_table(table).map_err(backend)?;
let last = t.last().map_err(backend)?;
let next = last.map(|(k, _)| k.value() + 1).unwrap_or(0);
Ok(next)
}
fn insert_blocking(
db: &Database,
table: TableDefinition<u64, (i64, &str, &str)>,
key: u64,
session_id: &str,
text: &str,
) -> Result<(), LogError> {
let txn = db.begin_write().map_err(backend)?;
{
let mut t = txn.open_table(table).map_err(backend)?;
t.insert(key, (now_unix(), session_id, text))
.map_err(backend)?;
}
txn.commit().map_err(backend)?;
Ok(())
}
impl RedbLog {
pub async fn connect(path: &Path) -> Result<Self, LogError> {
Self::connect_with_config(RedbLogConfig::new(path)).await
}
pub async fn connect_with_config(config: RedbLogConfig) -> Result<Self, LogError> {
let path = config.path;
let db = tokio::task::spawn_blocking(move || -> Result<Database, LogError> {
let db = Database::create(&path).map_err(backend)?;
let txn = db.begin_write().map_err(backend)?;
txn.open_table(INCOMING).map_err(backend)?;
txn.open_table(OUTGOING).map_err(backend)?;
txn.open_table(EVENT).map_err(backend)?;
txn.commit().map_err(backend)?;
Ok(db)
})
.await
.map_err(backend)??;
let db = Arc::new(db);
let mut next_incoming = next_key(&db, INCOMING)?;
let mut next_outgoing = next_key(&db, OUTGOING)?;
let mut next_event = next_key(&db, EVENT)?;
let (tx, mut rx) = mpsc::channel::<Entry>(ASYNC_LOG_CHANNEL_CAPACITY);
let session_id = config.session_id;
let task = tokio::spawn(async move {
while let Some(entry) = rx.recv().await {
let db = db.clone();
let session_id = session_id.clone();
let result = match entry {
Entry::Message { direction, text } => {
let (table, key) = if direction == "I" {
let key = next_incoming;
next_incoming += 1;
(INCOMING, key)
} else {
let key = next_outgoing;
next_outgoing += 1;
(OUTGOING, key)
};
tokio::task::spawn_blocking(move || {
insert_blocking(&db, table, key, &session_id, &text)
})
.await
}
Entry::Event { text } => {
let key = next_event;
next_event += 1;
tokio::task::spawn_blocking(move || {
insert_blocking(&db, EVENT, key, &session_id, &text)
})
.await
}
};
let _ = result;
}
});
Ok(Self {
tx: std::sync::Mutex::new(Some(tx)),
task: std::sync::Mutex::new(Some(task)),
include_heartbeats: config.include_heartbeats,
})
}
fn send(&self, entry: Entry) {
if let Ok(guard) = self.tx.lock()
&& let Some(tx) = guard.as_ref()
{
let _ = tx.try_send(entry);
}
}
}
#[async_trait::async_trait]
impl Log for RedbLog {
fn on_incoming(&self, message: &str) {
if is_heartbeat(message) && !self.include_heartbeats {
return;
}
self.send(Entry::Message {
direction: "I",
text: message.to_owned(),
});
}
fn on_outgoing(&self, message: &str) {
if is_heartbeat(message) && !self.include_heartbeats {
return;
}
self.send(Entry::Message {
direction: "O",
text: message.to_owned(),
});
}
fn on_event(&self, text: &str) {
self.send(Entry::Event {
text: text.to_owned(),
});
}
async fn shutdown(&self) {
let tx = self.tx.lock().ok().and_then(|mut guard| guard.take());
drop(tx);
let task = self.task.lock().ok().and_then(|mut guard| guard.take());
if let Some(task) = task {
let _ = task.await;
}
}
}