use std::net::SocketAddr;
use std::sync::Arc;
use nodedb_cluster::calvin::types::LockKeyWire;
use crate::bridge::envelope::PhysicalPlan;
use crate::control::cluster::calvin::scheduler::lock::LockKey;
use crate::control::planner::calvin::reservation::submit_reserve_read;
use crate::control::server::shared::plan_util::{extract_collection, plan_engine, read_key_of};
use crate::control::state::SharedState;
use crate::types::{DatabaseId, KeyRepr, Lsn, TenantId, VShardId};
use super::store::SessionStore;
pub use nodedb_types::calvin::EngineTag;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ReadKey {
Point { repr: KeyRepr },
Predicate,
IndexEq { field: String, value: String },
IndexRange {
field: String,
lo: Option<String>,
hi: Option<String>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReadSetEntry {
pub engine: EngineTag,
pub database_id: DatabaseId,
pub tenant_id: TenantId,
pub collection: String,
pub key: ReadKey,
pub read_lsn: Lsn,
pub read_version_lsn: Lsn,
}
pub struct ReadCapture<'a> {
pub plan: &'a PhysicalPlan,
pub watermarks: &'a [(VShardId, Lsn)],
pub read_version_lsn: Lsn,
pub found: bool,
}
pub async fn record_read_set(
state: &SharedState,
sessions: &SessionStore,
addr: &SocketAddr,
tenant_id: TenantId,
capture: ReadCapture<'_>,
) {
let ReadCapture {
plan,
watermarks,
read_version_lsn,
found,
} = capture;
if watermarks.is_empty() {
return;
}
let engine = plan_engine(plan);
let key = read_key_of(plan, found);
let collection = extract_collection(plan)
.map(String::from)
.unwrap_or_default();
let database_id = sessions
.get_current_database(addr)
.unwrap_or(DatabaseId::DEFAULT);
let own_write_version = sessions.own_write_version(addr, database_id, tenant_id, &collection);
let effective_read_version = read_version_lsn.max(own_write_version);
let entries: Vec<ReadSetEntry> = watermarks
.iter()
.map(|(_vshard, read_lsn)| ReadSetEntry {
engine,
database_id,
tenant_id,
collection: collection.clone(),
key: key.clone(),
read_lsn: *read_lsn,
read_version_lsn: effective_read_version,
})
.collect();
sessions.record_read_entries(addr, entries);
if !sessions.is_in_transaction_block(addr) {
return;
}
let Some(lock_key) = lock_key_of_read(&key, &collection) else {
return;
};
let now = std::time::Instant::now();
let hot = {
let table = state
.hot_key_table
.lock()
.unwrap_or_else(|p| p.into_inner());
table.is_hot(&lock_key, now)
};
if !hot {
return;
}
let vshard = VShardId::from_collection_in_database(database_id, &collection).as_u32();
let owner = sessions.current_reservation_owner(addr);
let wire_key = lock_key_to_wire(&lock_key);
match submit_reserve_read(state, wire_key, vshard, owner).await {
Ok(r) => sessions.record_reservation(addr, vshard, r),
Err(e) => {
tracing::debug!(error = %e, "hot-key read reservation failed; proceeding under OCC");
}
}
}
pub(super) fn lock_key_of_read(key: &ReadKey, collection: &str) -> Option<LockKey> {
match key {
ReadKey::Point {
repr: KeyRepr::Surrogate(s),
} => Some(LockKey::Surrogate {
collection: Arc::from(collection),
surrogate: *s,
}),
ReadKey::Point {
repr: KeyRepr::KvKey(k),
} => Some(LockKey::Kv {
collection: Arc::from(collection),
key: Arc::from(&**k),
}),
_ => None,
}
}
fn lock_key_to_wire(key: &LockKey) -> LockKeyWire {
match key {
LockKey::Surrogate {
collection,
surrogate,
} => LockKeyWire::Surrogate {
collection: collection.to_string(),
surrogate: *surrogate,
},
LockKey::Kv { collection, key } => LockKeyWire::Kv {
collection: collection.to_string(),
key: key.to_vec(),
},
LockKey::Edge {
collection,
src,
dst,
} => LockKeyWire::Edge {
collection: collection.to_string(),
src: *src,
dst: *dst,
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use nodedb_physical::physical_plan::{DocumentOp, KvOp};
fn addr() -> SocketAddr {
"127.0.0.1:5599".parse().expect("test addr")
}
fn kv_get(collection: &str, key: &[u8]) -> PhysicalPlan {
PhysicalPlan::Kv(KvOp::Get {
collection: collection.to_string(),
key: key.to_vec(),
rls_filters: Vec::new(),
surrogate_ceiling: None,
})
}
fn kv_batch_get(collection: &str) -> PhysicalPlan {
PhysicalPlan::Kv(KvOp::BatchGet {
collection: collection.to_string(),
keys: vec![b"a".to_vec(), b"b".to_vec()],
})
}
fn begun_session() -> (SessionStore, SocketAddr) {
let sessions = SessionStore::new();
let a = addr();
sessions.ensure_session(a);
sessions.begin(&a, Lsn::new(5), 0).expect("begin");
(sessions, a)
}
fn test_state() -> (std::sync::Arc<SharedState>, tempfile::TempDir) {
use crate::bridge::dispatch::Dispatcher;
use crate::wal::WalManager;
let dir = tempfile::tempdir().expect("tempdir");
let wal = std::sync::Arc::new(
WalManager::open_for_testing(&dir.path().join("test.wal")).expect("wal"),
);
let (dispatcher, _data_sides) = Dispatcher::new(1, 64);
let state = SharedState::new(dispatcher, wal).expect("shared state");
(state, dir)
}
#[tokio::test]
async fn point_read_records_point_key() {
let (state, _dir) = test_state();
let (sessions, a) = begun_session();
record_read_set(
&state,
&sessions,
&a,
TenantId::new(1),
ReadCapture {
plan: &kv_get("c", b"k1"),
watermarks: &[(VShardId::new(0), Lsn::new(7))],
read_version_lsn: Lsn::ZERO,
found: true,
},
)
.await;
let rs = sessions.take_read_set(&a);
assert_eq!(rs.len(), 1);
assert_eq!(rs[0].engine, EngineTag::Kv);
assert_eq!(rs[0].collection, "c");
assert_eq!(rs[0].read_lsn, Lsn::new(7));
assert_eq!(
rs[0].key,
ReadKey::Point {
repr: KeyRepr::KvKey(Box::from(b"k1".as_slice())),
}
);
}
#[tokio::test]
async fn predicate_read_records_predicate_key() {
let (state, _dir) = test_state();
let (sessions, a) = begun_session();
record_read_set(
&state,
&sessions,
&a,
TenantId::new(1),
ReadCapture {
plan: &kv_batch_get("c"),
watermarks: &[(VShardId::new(0), Lsn::new(9))],
read_version_lsn: Lsn::ZERO,
found: true,
},
)
.await;
let rs = sessions.take_read_set(&a);
assert_eq!(rs.len(), 1);
assert_eq!(rs[0].key, ReadKey::Predicate);
}
#[tokio::test]
async fn multi_shard_read_records_one_entry_per_watermark() {
let (state, _dir) = test_state();
let (sessions, a) = begun_session();
record_read_set(
&state,
&sessions,
&a,
TenantId::new(1),
ReadCapture {
plan: &kv_batch_get("c"),
watermarks: &[
(VShardId::new(0), Lsn::new(3)),
(VShardId::new(1), Lsn::new(11)),
(VShardId::new(2), Lsn::new(7)),
],
read_version_lsn: Lsn::ZERO,
found: true,
},
)
.await;
let rs = sessions.take_read_set(&a);
assert_eq!(rs.len(), 3);
let mut lsns: Vec<u64> = rs.iter().map(|e| e.read_lsn.as_u64()).collect();
lsns.sort_unstable();
assert_eq!(lsns, vec![3, 7, 11]);
}
#[tokio::test]
async fn absent_key_point_read_is_recorded() {
let (state, _dir) = test_state();
let (sessions, a) = begun_session();
record_read_set(
&state,
&sessions,
&a,
TenantId::new(1),
ReadCapture {
plan: &kv_get("c", b"missing"),
watermarks: &[(VShardId::new(0), Lsn::new(5))],
read_version_lsn: Lsn::ZERO,
found: false,
},
)
.await;
let rs = sessions.take_read_set(&a);
assert_eq!(rs.len(), 1);
assert_eq!(
rs[0].key,
ReadKey::Point {
repr: KeyRepr::KvKey(Box::from(b"missing".as_slice())),
}
);
}
fn doc_point_get(collection: &str, surrogate: u32) -> PhysicalPlan {
PhysicalPlan::Document(DocumentOp::PointGet {
collection: collection.to_string(),
document_id: "d".to_string(),
surrogate: nodedb_types::Surrogate::new(surrogate),
pk_bytes: Vec::new(),
rls_filters: Vec::new(),
system_time: Default::default(),
valid_at_ms: None,
})
}
#[tokio::test]
async fn document_point_read_hit_records_precise_surrogate() {
let (state, _dir) = test_state();
let (sessions, a) = begun_session();
record_read_set(
&state,
&sessions,
&a,
TenantId::new(1),
ReadCapture {
plan: &doc_point_get("docs", 42),
watermarks: &[(VShardId::new(0), Lsn::new(7))],
read_version_lsn: Lsn::ZERO,
found: true,
},
)
.await;
let rs = sessions.take_read_set(&a);
assert_eq!(rs.len(), 1);
assert_eq!(
rs[0].key,
ReadKey::Point {
repr: KeyRepr::Surrogate(42),
}
);
}
#[tokio::test]
async fn absent_document_point_read_records_predicate() {
let (state, _dir) = test_state();
let (sessions, a) = begun_session();
record_read_set(
&state,
&sessions,
&a,
TenantId::new(1),
ReadCapture {
plan: &doc_point_get("docs", 999),
watermarks: &[(VShardId::new(0), Lsn::new(5))],
read_version_lsn: Lsn::ZERO,
found: false,
},
)
.await;
let rs = sessions.take_read_set(&a);
assert_eq!(rs.len(), 1);
assert_eq!(rs[0].key, ReadKey::Predicate);
}
#[tokio::test]
async fn autocommit_reads_are_not_recorded() {
let (state, _dir) = test_state();
let sessions = SessionStore::new();
let a = addr();
sessions.ensure_session(a);
record_read_set(
&state,
&sessions,
&a,
TenantId::new(1),
ReadCapture {
plan: &kv_get("c", b"k1"),
watermarks: &[(VShardId::new(0), Lsn::new(7))],
read_version_lsn: Lsn::ZERO,
found: true,
},
)
.await;
assert!(sessions.take_read_set(&a).is_empty());
}
#[tokio::test]
async fn empty_watermarks_records_nothing() {
let (state, _dir) = test_state();
let (sessions, a) = begun_session();
record_read_set(
&state,
&sessions,
&a,
TenantId::new(1),
ReadCapture {
plan: &kv_get("c", b"k1"),
watermarks: &[],
read_version_lsn: Lsn::ZERO,
found: true,
},
)
.await;
assert!(sessions.take_read_set(&a).is_empty());
}
#[test]
fn point_get_document_uses_surrogate_identity() {
let plan = PhysicalPlan::Document(DocumentOp::PointGet {
collection: "docs".to_string(),
document_id: "d1".to_string(),
surrogate: nodedb_types::Surrogate::new(42),
pk_bytes: Vec::new(),
rls_filters: Vec::new(),
system_time: Default::default(),
valid_at_ms: None,
});
assert_eq!(
read_key_of(&plan, true),
ReadKey::Point {
repr: KeyRepr::Surrogate(42),
}
);
assert_eq!(plan_engine(&plan), EngineTag::Document);
}
fn indexed_fetch(collection: &str, path: &str, value: &str) -> PhysicalPlan {
PhysicalPlan::Document(DocumentOp::IndexedFetch {
collection: collection.to_string(),
path: path.to_string(),
value: value.to_string(),
filters: Vec::new(),
projection: Vec::new(),
limit: 0,
offset: 0,
})
}
fn range_scan(
collection: &str,
field: &str,
lower: Option<&[u8]>,
upper: Option<&[u8]>,
) -> PhysicalPlan {
PhysicalPlan::Document(DocumentOp::RangeScan {
collection: collection.to_string(),
field: field.to_string(),
lower: lower.map(|b| b.to_vec()),
upper: upper.map(|b| b.to_vec()),
limit: 0,
})
}
#[test]
fn indexed_fetch_always_records_index_eq() {
let plan = indexed_fetch("users", "$.email", "a@b.c");
assert_eq!(
read_key_of(&plan, true),
ReadKey::IndexEq {
field: "$.email".to_string(),
value: "a@b.c".to_string(),
}
);
}
#[test]
fn range_scan_always_records_index_range() {
let plan = range_scan("users", "$.age", Some(b"18"), Some(b"65"));
assert_eq!(
read_key_of(&plan, true),
ReadKey::IndexRange {
field: "$.age".to_string(),
lo: Some("18".to_string()),
hi: Some("65".to_string()),
}
);
}
}