use super::read::upd_payload_rows;
use super::{KdbConnection, KdbDeserialize, KdbExt, SymbolInterner};
use crate::RunMode;
use crate::nodes::produce_async;
use crate::types::*;
use anyhow::Context;
use kdb_plus_fixed::ipc::{ConnectionMethod, QStream};
use kdb_plus_fixed::qtype;
use log::info;
use std::rc::Rc;
#[must_use]
pub fn kdb_sub<T>(
connection: KdbConnection,
table: impl Into<String>,
symbols: impl Into<String>,
) -> Rc<dyn Stream<Burst<T>>>
where
T: Element + Send + KdbDeserialize + 'static,
{
let table = table.into();
let symbols = symbols.into();
produce_async(
move |ctx| {
let run_mode = ctx.run_mode;
let connection = connection;
let table = table;
let symbols = symbols;
async move {
if !matches!(run_mode, RunMode::RealTime) {
anyhow::bail!(
"kdb_sub requires RunMode::RealTime; use kdb_read for historical replay"
);
}
let creds = connection.credentials_string();
let mut socket = QStream::connect(
ConnectionMethod::TCP,
&connection.host,
connection.port,
&creds,
)
.await
.with_context(|| {
format!(
"kdb_sub: failed to connect to {}:{}",
connection.host, connection.port
)
})?;
let sub_query = format!(".u.sub[`{table};{symbols}]");
info!("kdb_sub: {sub_query}");
let sub_reply = socket
.send_sync_message(&sub_query.as_str())
.await
.with_context(|| format!("kdb_sub: subscription `{sub_query}` failed"))?;
let columns: Vec<String> = sub_reply
.element_at(1)
.ok()
.and_then(|schema| schema.column_names().ok())
.unwrap_or_default();
Ok(async_stream::stream! {
let mut interner = SymbolInterner::default();
loop {
let (_msg_type, msg) = match socket.receive_message().await {
Ok(m) => m,
Err(e) => {
yield Err(anyhow::Error::new(e).context("kdb_sub: receive failed"));
break;
}
};
if msg.get_type() != qtype::COMPOUND_LIST || msg.len() < 3 {
continue;
}
match msg.element_at(0).ok().as_ref().and_then(|f| f.get_symbol().ok()) {
Some("upd") => {}
_ => continue,
}
let data = match msg.element_at(2) {
Ok(d) => d,
Err(e) => {
yield Err(anyhow::Error::new(e).context("kdb_sub: upd message has no data"));
break;
}
};
let rows = match upd_payload_rows(&data) {
Ok(rows) => rows,
Err(e) => { yield Err(e); break; }
};
for row in &rows {
match T::from_kdb_row(row, &columns, &mut interner) {
Ok((time, record)) => yield Ok((time, record)),
Err(e) => { yield Err(anyhow::Error::new(e)); return; }
}
}
}
})
}
},
None,
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::adapters::kdb::{KdbError, Row};
use crate::nodes::{NodeOperators, StreamOperators};
use crate::{RunFor, RunMode};
#[derive(Debug, Clone, Default)]
struct TestRow;
impl KdbDeserialize for TestRow {
fn from_kdb_row(
_row: Row<'_>,
_columns: &[String],
_interner: &mut SymbolInterner,
) -> std::result::Result<(NanoTime, Self), KdbError> {
Ok((NanoTime::ZERO, TestRow))
}
}
#[test]
fn test_sub_rejects_historical_mode() {
let result = kdb_sub::<TestRow>(KdbConnection::new("127.0.0.1", 1), "trades", "`")
.collapse()
.collect()
.run(
RunMode::HistoricalFrom(NanoTime::from_kdb_timestamp(0)),
RunFor::Cycles(1),
);
let err = result.expect_err("historical mode must be rejected");
assert!(
format!("{err:#}").contains("requires RunMode::RealTime"),
"unexpected error: {err:#}"
);
}
}