use super::integration_tests::{TABLE_NAME, slice_query, with_test_data};
use super::*;
use crate::adapters::cache::CacheConfig;
use crate::{RunFor, RunMode, nodes::*, types::*};
use anyhow::Result;
const PERIOD: std::time::Duration = std::time::Duration::from_secs(24 * 3600);
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
struct TestTradeCached {
sym: Sym,
price: f64,
qty: i64,
}
impl KdbDeserialize for TestTradeCached {
fn from_kdb_row(
row: Row<'_>,
_columns: &[String],
interner: &mut SymbolInterner,
) -> Result<(NanoTime, Self), KdbError> {
let time = row.get_timestamp(1)?; Ok((
time,
TestTradeCached {
sym: row.get_sym(2, interner)?,
price: row.get(3)?.get_float()?,
qty: row.get(4)?.get_long()?,
},
))
}
}
fn run_cached(conn: KdbConnection, cache_dir: &std::path::Path) -> Result<usize> {
let stream = kdb_read_cached::<TestTradeCached>(
conn,
PERIOD,
CacheConfig::new(cache_dir, u64::MAX),
|within, date, _| slice_query(date, within.0, within.1),
);
let collected = stream.collapse().collect();
collected.clone().run(
RunMode::HistoricalFrom(NanoTime::from_kdb_timestamp(0)),
RunFor::Duration(std::time::Duration::from_secs(86400)),
)?;
Ok(collected.peek_value().len())
}
fn count_cache_files(cache_dir: &std::path::Path) -> usize {
std::fs::read_dir(cache_dir)
.into_iter()
.flatten()
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().map(|x| x == "cache").unwrap_or(false))
.count()
}
#[test]
fn test_kdb_read_cached_populates_and_hits() -> Result<()> {
let _ = env_logger::try_init();
let cache_dir =
std::env::temp_dir().join(format!("wingfoil_cache_integ_{}", std::process::id()));
with_test_data(3, 1, true, |_n, conn| {
let n = run_cached(conn, &cache_dir)?;
assert_eq!(n, 3, "First run should read 3 rows from KDB");
assert!(
count_cache_files(&cache_dir) > 0,
"Cache directory should contain .cache files after first run"
);
Ok(())
})?;
let closed = KdbConnection::new("localhost", 59999);
let n = run_cached(closed, &cache_dir)?;
assert_eq!(n, 3, "Second run should return same 3 rows from cache");
std::fs::remove_dir_all(&cache_dir).ok();
Ok(())
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
struct TickCached {
sym: Sym,
price: f64,
qty: i64,
}
impl KdbDeserialize for TickCached {
fn from_kdb_row(
row: Row<'_>,
_columns: &[String],
interner: &mut SymbolInterner,
) -> Result<(NanoTime, Self), KdbError> {
let time = row.get_timestamp(0)?; Ok((
time,
TickCached {
sym: row.get_sym(1, interner)?,
price: row.get(2)?.get_float()?,
qty: row.get(3)?.get_long()?,
},
))
}
}
#[test]
fn test_kdb_read_cached_drops_rows_outside_window() -> Result<()> {
let _ = env_logger::try_init();
let conn = super::integration_tests::test_connection();
let cache_dir =
std::env::temp_dir().join(format!("wingfoil_cache_window_{}", std::process::id()));
std::fs::remove_dir_all(&cache_dir).ok();
const CTBL: &str = "cached_window_trades";
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async {
let mut s = super::integration_tests::connect(&conn).await?;
super::integration_tests::q_exec(
&mut s,
&format!("{CTBL}:([]time:`timestamp$();sym:`symbol$();price:`float$();qty:`long$())"),
)
.await?;
super::integration_tests::q_exec(
&mut s,
&format!(
"insert[`{CTBL};(2000.01.01D00:00:00+1000000000*75 90 150 210;\
`EARLY`AAPL`GOOG`MSFT;100 101 102 103f;10 20 30 40j)]"
),
)
.await?;
Ok::<(), anyhow::Error>(())
})?;
let period = std::time::Duration::from_secs(60);
let run = |conn: KdbConnection, start: NanoTime, dur_secs: u64| -> Result<Vec<(String, i64)>> {
let stream = kdb_read_cached::<TickCached>(
conn,
period,
CacheConfig::new(&cache_dir, u64::MAX),
move |(t0, t1), _, _| {
format!(
"select from {CTBL} where time >= (`timestamp$){}j, time < (`timestamp$){}j",
t0.to_kdb_timestamp(),
t1.to_kdb_timestamp(),
)
},
);
let collected = stream.collect();
collected.clone().run(
RunMode::HistoricalFrom(start),
RunFor::Duration(std::time::Duration::from_secs(dur_secs)),
)?;
Ok(collected
.peek_value()
.iter()
.flat_map(|va| va.value.iter())
.map(|t| (t.sym.to_string(), t.qty))
.collect())
};
let start_90 = NanoTime::from_kdb_timestamp(90 * 1_000_000_000);
let start_70 = NanoTime::from_kdb_timestamp(70 * 1_000_000_000);
let closed = KdbConnection::new("localhost", 59999);
let first = run(conn.clone(), start_90, 180)?;
let second = run(closed.clone(), start_90, 180)?;
let third = run(closed, start_70, 180)?;
rt.block_on(async {
let mut s = super::integration_tests::connect(&conn).await?;
super::integration_tests::q_exec(&mut s, &format!("delete {CTBL} from `.")).await?;
Ok::<(), anyhow::Error>(())
})?;
std::fs::remove_dir_all(&cache_dir).ok();
let in_window = vec![
("AAPL".to_string(), 20),
("GOOG".to_string(), 30),
("MSFT".to_string(), 40),
];
assert_eq!(
first, in_window,
"cache-miss path must drop the pre-start row"
);
assert_eq!(
second, in_window,
"cache-hit path must also drop the pre-start row"
);
assert_eq!(
third,
vec![
("EARLY".to_string(), 10),
("AAPL".to_string(), 20),
("GOOG".to_string(), 30),
("MSFT".to_string(), 40),
],
"cache stored the full slice: a wider window surfaces the 75s row from cache"
);
Ok(())
}
#[test]
fn test_kdb_read_cached_corrupt_fallback() -> Result<()> {
let _ = env_logger::try_init();
let cache_dir =
std::env::temp_dir().join(format!("wingfoil_cache_corrupt_{}", std::process::id()));
with_test_data(3, 1, true, |_n, conn| {
let n = run_cached(conn.clone(), &cache_dir)?;
assert_eq!(n, 3);
let corrupt_path = std::fs::read_dir(&cache_dir)?
.filter_map(|e| e.ok())
.find(|e| e.path().extension().map(|x| x == "cache").unwrap_or(false))
.expect("should have a cache file")
.path();
std::fs::write(
&corrupt_path,
format!("select from {TABLE_NAME}\ngarbage not valid bincode"),
)?;
let n = run_cached(conn, &cache_dir)?;
assert_eq!(n, 3, "Fallback run should still return 3 rows");
Ok(())
})?;
let closed = KdbConnection::new("localhost", 59999);
let n = run_cached(closed, &cache_dir)?;
assert_eq!(n, 3, "After corrupt-file overwrite, cache hit should work");
std::fs::remove_dir_all(&cache_dir).ok();
Ok(())
}
#[test]
fn test_kdb_read_cached_partial_cache() -> Result<()> {
let _ = env_logger::try_init();
let cache_dir =
std::env::temp_dir().join(format!("wingfoil_cache_partial_{}", std::process::id()));
let half_day = std::time::Duration::from_secs(12 * 3600);
let run = |conn: KdbConnection| -> Result<usize> {
let stream = kdb_read_cached::<TestTradeCached>(
conn,
half_day,
CacheConfig::new(&cache_dir, u64::MAX),
|within, date, _| slice_query(date, within.0, within.1),
);
let collected = stream.collapse().collect();
collected.clone().run(
RunMode::HistoricalFrom(NanoTime::from_kdb_timestamp(0)),
RunFor::Duration(std::time::Duration::from_secs(2 * 86400)),
)?;
Ok(collected.peek_value().len())
};
with_test_data(4, 2, true, |n, conn| {
let count = run(conn.clone())?;
assert_eq!(count, n, "First run should read all rows from KDB");
let victim = std::fs::read_dir(&cache_dir)?
.filter_map(|e| e.ok())
.find(|e| e.path().extension().map(|x| x == "cache").unwrap_or(false))
.expect("should have cache files")
.path();
std::fs::remove_file(&victim)?;
let count2 = run(conn)?;
assert_eq!(count2, n, "Partial-cache run should still return all rows");
Ok(())
})?;
let closed = KdbConnection::new("localhost", 59999);
let n = run(closed)?;
assert!(
n > 0,
"All slices cached: closed port should not be dialled"
);
std::fs::remove_dir_all(&cache_dir).ok();
Ok(())
}