use super::{KdbConnection, KdbDeserialize, KdbExt, SymbolInterner};
use crate::adapters::cache::{CacheConfig, CacheKey, FileCache};
use crate::adapters::common::{TimeWindow, WindowFilter, compute_validated_time_slices};
use crate::nodes::produce_async;
use crate::types::*;
use kdb_plus_fixed::ipc::{ConnectionMethod, K, QStream};
use log::info;
use std::rc::Rc;
#[must_use]
pub fn kdb_read_cached<T>(
connection: KdbConnection,
period: std::time::Duration,
cache_config: CacheConfig,
query_fn: impl FnMut((NanoTime, NanoTime), i32, usize) -> String + Send + 'static,
) -> Rc<dyn Stream<Burst<T>>>
where
T: Element
+ Send
+ Sync
+ KdbDeserialize
+ serde::Serialize
+ for<'de> serde::Deserialize<'de>
+ 'static,
{
produce_async(
move |ctx| {
let start_time = ctx.start_time;
let end_time_result = ctx.end_time();
async move {
let end_time_bound = end_time_result.as_ref().ok().copied();
let slices = compute_validated_time_slices(
"kdb_read_cached",
start_time,
end_time_result,
period,
)?;
let end_time = end_time_bound
.expect("compute_validated_time_slices accepted a bounded end_time");
tokio::fs::create_dir_all(&cache_config.folder).await?;
let cache = FileCache::<T>::new(cache_config);
Ok(async_stream::stream! {
let mut socket: Option<QStream> = None;
let mut interner = SymbolInterner::default();
let mut query_fn = query_fn;
'slices: for (within, date, iteration) in slices {
let (t0, t1) = within;
let window = TimeWindow::clamp(t0, t1, start_time, end_time);
let query = query_fn(within, date, iteration);
let key = CacheKey::from_parts(&[&query]);
let cached = match cache.get(&key).await {
Ok(Some(rows)) => Some(rows),
Ok(None) => None,
Err(e) => {
log::warn!("KDB cache read error (falling back to KDB): {e}");
None
}
};
if let Some(rows) = cached {
let mut filter = WindowFilter::new("kdb_read_cached", window);
for (time, record) in rows {
if !filter.keep(time) {
continue;
}
yield Ok((time, record));
}
filter.finish();
continue;
}
if socket.is_none() {
let creds = connection.credentials_string();
match QStream::connect(
ConnectionMethod::TCP,
&connection.host,
connection.port,
&creds,
)
.await
{
Ok(s) => socket = Some(s),
Err(e) => {
yield Err(e.into());
break 'slices;
}
}
}
let sock = socket
.as_mut()
.expect("socket initialised on cache miss above");
info!("KDB query: {query}");
let fetch_start = std::time::Instant::now();
let result: K = match sock.send_sync_message(&query.as_str()).await {
Ok(r) => r,
Err(e) => {
yield Err(e.into());
break 'slices;
}
};
let (columns, rows) = match (result.column_names(), result.rows()) {
(Ok(cols), Ok(rows)) => (cols, rows),
(Err(e), _) | (_, Err(e)) => {
yield Err(e);
break 'slices;
}
};
let row_count = rows.len();
info!("KDB query: {} rows in {:?}", row_count, fetch_start.elapsed());
let mut parsed: Vec<(NanoTime, T)> = Vec::with_capacity(row_count);
let mut prev_time: Option<NanoTime> = None;
for row in &rows {
let (time, record) = match T::from_kdb_row(row, &columns, &mut interner) {
Ok(r) => r,
Err(e) => {
yield Err(e.into());
break 'slices;
}
};
if let Some(prev) = prev_time
&& time < prev
{
yield Err(anyhow::anyhow!(
"KDB data is not sorted by time: got {time:?} after {prev:?}. \
Add `xasc` to your query to sort the data."
));
break 'slices;
}
prev_time = Some(time);
parsed.push((time, record));
}
if let Err(e) = cache.put(&key, &query, &parsed).await {
log::warn!("KDB cache write error: {e}");
}
let mut filter = WindowFilter::new("kdb_read_cached", window);
for (time, record) in parsed {
if !filter.keep(time) {
continue;
}
yield Ok((time, record));
}
filter.finish();
}
})
}
},
None,
)
}