use std::sync::Arc;
use anyhow::Result;
use reblessive::tree::Stk;
use tokio::sync::OnceCell;
use super::document::Extras;
use crate::cnf::LiveQueryEngine;
use crate::ctx::FrozenContext;
use crate::dbs::Options;
use crate::doc::{Action, CursorDoc, Document, DocumentContext};
use crate::idx::planner::RecordStrategy;
use crate::lq::event::{LiveAction, LiveEvent};
impl Document {
pub(super) async fn process_live_events(
&self,
ctx: &FrozenContext,
opt: &Options,
) -> Result<()> {
if ctx.config.live_query_engine != LiveQueryEngine::Router {
return Ok(());
}
if opt.import {
return Ok(());
}
if !self.is_modified() {
return Ok(());
}
let ns = self.doc_ctx.ns();
let db = self.doc_ctx.db();
let tb = self.doc_ctx.tb()?;
if !ctx.tx().table_has_live_query(ns.namespace_id, db.database_id, &tb.name).await? {
return Ok(());
}
if let Some(id) = &self.id {
ctx.tx().live_event_buffer_record_change(
ns.namespace_id,
db.database_id,
&tb.name,
id.as_ref(),
self.initial.doc.clone(),
self.current.doc.clone(),
);
}
Ok(())
}
pub(crate) async fn replay_live_event(
stk: &mut Stk,
ctx: &FrozenContext,
opt: &Options,
doc_ctx: DocumentContext,
event: &LiveEvent,
) -> Result<()> {
let action = match event.action {
LiveAction::Create => Action::Create,
LiveAction::Update => Action::Update,
LiveAction::Delete => Action::Delete,
};
let id = Arc::new(event.id.clone());
let initial = CursorDoc::new(Some(Arc::clone(&id)), None, event.before.clone());
let current = CursorDoc::new(Some(Arc::clone(&id)), None, event.after.clone());
let mut doc = Document {
doc_ctx,
id: Some(id),
r#gen: None,
retry: false,
extras: Extras::Normal,
initial,
current,
initial_reduced: None,
current_reduced: None,
record_strategy: RecordStrategy::KeysAndValues,
input_data: None,
mutated: false,
modified: OnceCell::new(),
};
doc.process_table_lives_inner(stk, ctx, opt, action).await
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use std::time::Duration;
use crate::catalog::providers::CatalogProvider;
use crate::catalog::{DatabaseId, NamespaceId};
use crate::cnf::ConfigMap;
use crate::dbs::{Capabilities, Session};
use crate::key::lqe;
use crate::kvs::LockType::Optimistic;
use crate::kvs::TransactionType::{Read, Write};
use crate::kvs::{Datastore, KVKey, KVValue};
use crate::lq::event::{LiveAction, LiveEvent, LiveEvents};
use crate::types::PublicValue;
async fn new_ds(engine: &str) -> Datastore {
build_ds(engine, None).await
}
async fn build_ds(engine: &str, retention: Option<&str>) -> Datastore {
let (send, _recv) = crate::channel::bounded(1000);
let mut config = ConfigMap::empty().with_key_value("live_query_engine", engine);
if let Some(r) = retention {
config = config.with_key_value("live_query_retention", r);
}
Datastore::builder()
.with_capabilities(Capabilities::all())
.with_auth(false)
.with_notify(send)
.with_config(config)
.build_with_path("memory")
.await
.unwrap()
}
async fn setup(ds: &Datastore, ns: &str, db: &str, tb: &str) -> (NamespaceId, DatabaseId) {
let tx = ds.transaction(Write, Optimistic).await.unwrap();
let dbdef = tx.ensure_ns_db(None, ns, db).await.unwrap();
tx.commit().await.unwrap();
let ses = Session::owner().with_ns(ns).with_db(db);
ds.execute(&format!("DEFINE TABLE {tb}"), &ses, None).await.unwrap();
(dbdef.namespace_id, dbdef.database_id)
}
async fn live_events(ds: &Datastore, ns: NamespaceId, db: DatabaseId) -> Vec<LiveEvent> {
let tx = ds.transaction(Read, Optimistic).await.unwrap();
let beg = lqe::prefix(ns, db).encode_key().unwrap();
let end = lqe::suffix(ns, db).encode_key().unwrap();
let mut events = Vec::new();
for (_k, v) in tx.scan(beg..end, 1000, 0, None).await.unwrap() {
events.extend(LiveEvents::kv_decode_value(&v, ()).unwrap().0);
}
tx.cancel().await.unwrap();
events
}
#[tokio::test]
async fn router_captures_live_events_only_for_subscribed_tables() {
let ds = new_ds("router").await;
let (ns, db, tb) = ("test", "test", "person");
let (ns_id, db_id) = setup(&ds, ns, db, tb).await;
let owner = Session::owner().with_ns(ns).with_db(db);
ds.execute(&format!("CREATE {tb}:0 SET n = 0"), &owner, None).await.unwrap();
assert!(
live_events(&ds, ns_id, db_id).await.is_empty(),
"nothing should be captured before a subscriber exists"
);
let live = Session::owner().with_ns(ns).with_db(db).with_rt(true);
ds.execute(&format!("LIVE SELECT * FROM {tb}"), &live, None).await.unwrap();
ds.execute(&format!("CREATE {tb}:1 SET n = 1"), &owner, None).await.unwrap();
let events = live_events(&ds, ns_id, db_id).await;
assert_eq!(events.len(), 1, "exactly one live event captured for the subscribed write");
assert_eq!(events[0].action, LiveAction::Create);
assert!(events[0].before.is_none(), "create has no before value");
assert!(!events[0].after.is_none(), "create carries the after value");
}
#[tokio::test]
async fn router_captures_for_subscription_created_on_another_node() {
let ds_a = new_ds("router").await;
let (ns, db, tb) = ("test", "test", "person");
let (ns_id, db_id) = setup(&ds_a, ns, db, tb).await;
let ds_b = ds_a.fork_for_test_with_node_id(uuid::Uuid::new_v4());
let live = Session::owner().with_ns(ns).with_db(db).with_rt(true);
ds_b.execute(&format!("LIVE SELECT * FROM {tb}"), &live, None).await.unwrap();
let owner = Session::owner().with_ns(ns).with_db(db);
ds_a.execute(&format!("CREATE {tb}:1 SET n = 1"), &owner, None).await.unwrap();
let events = live_events(&ds_a, ns_id, db_id).await;
assert_eq!(
events.len(),
1,
"node A must capture an event for a subscription created on node B",
);
assert_eq!(events[0].action, LiveAction::Create);
}
#[tokio::test]
async fn router_retains_before_values_for_update_and_delete() {
let ds = new_ds("router").await;
let (ns, db, tb) = ("test", "test", "person");
let (ns_id, db_id) = setup(&ds, ns, db, tb).await;
let live = Session::owner().with_ns(ns).with_db(db).with_rt(true);
ds.execute(&format!("LIVE SELECT * FROM {tb}"), &live, None).await.unwrap();
let owner = Session::owner().with_ns(ns).with_db(db);
ds.execute(&format!("CREATE {tb}:1 SET n = 1"), &owner, None).await.unwrap();
ds.execute(&format!("UPDATE {tb}:1 SET n = 2"), &owner, None).await.unwrap();
ds.execute(&format!("DELETE {tb}:1"), &owner, None).await.unwrap();
let events = live_events(&ds, ns_id, db_id).await;
let update = events
.iter()
.find(|e| e.action == LiveAction::Update)
.expect("an update event should be captured");
assert!(!update.before.is_none(), "update retains the before value");
assert!(!update.after.is_none(), "update carries the after value");
let delete = events
.iter()
.find(|e| e.action == LiveAction::Delete)
.expect("a delete event should be captured");
assert!(!delete.before.is_none(), "delete retains the before value (pre-image)");
}
#[tokio::test]
async fn gc_collects_events_after_subscribers_leave() {
let ds = build_ds("router", Some("1us")).await;
ds.bootstrap().await.unwrap();
let (ns, db, tb) = ("test", "test", "person");
let (ns_id, db_id) = setup(&ds, ns, db, tb).await;
let live = Session::owner().with_ns(ns).with_db(db).with_rt(true);
let mut res = ds.execute(&format!("LIVE SELECT * FROM {tb}"), &live, None).await.unwrap();
let lqid = match res.remove(0).result.unwrap() {
PublicValue::Uuid(u) => u,
other => panic!("LIVE should return a uuid, got {other:?}"),
};
let owner = Session::owner().with_ns(ns).with_db(db);
ds.execute(&format!("CREATE {tb}:1 SET n = 1"), &owner, None).await.unwrap();
assert_eq!(
live_events(&ds, ns_id, db_id).await.len(),
1,
"event captured while subscribed"
);
ds.execute(&format!("KILL u'{lqid}'"), &live, None).await.unwrap();
tokio::time::sleep(Duration::from_millis(500)).await;
ds.changefeed_process(&Duration::from_secs(1)).await.unwrap();
assert!(
live_events(&ds, ns_id, db_id).await.is_empty(),
"orphaned live-query events must be GC'd by retention even with no current subscriber"
);
}
#[tokio::test]
async fn inline_engine_captures_no_live_events() {
let ds = new_ds("inline").await;
let (ns, db, tb) = ("test", "test", "person");
let (ns_id, db_id) = setup(&ds, ns, db, tb).await;
let live = Session::owner().with_ns(ns).with_db(db).with_rt(true);
ds.execute(&format!("LIVE SELECT * FROM {tb}"), &live, None).await.unwrap();
let owner = Session::owner().with_ns(ns).with_db(db);
ds.execute(&format!("CREATE {tb}:1 SET n = 1"), &owner, None).await.unwrap();
assert!(
live_events(&ds, ns_id, db_id).await.is_empty(),
"the Inline engine must not write to the live-query keyspace"
);
}
}