use tracing::warn;
use nodedb_wal::WalRecord;
use nodedb_wal::record::RecordType;
use super::core_loop::CoreLoop;
use crate::types::DatabaseId;
impl CoreLoop {
pub(crate) fn try_replay_graph_node_label(
&mut self,
record: &WalRecord,
database_id: DatabaseId,
) -> Option<usize> {
let record_type = RecordType::from_raw(record.logical_record_type())?;
let is_set = record_type == RecordType::GraphNodeLabelSet;
let is_remove = record_type == RecordType::GraphNodeLabelRemove;
if !is_set && !is_remove {
return None;
}
let tenant_id = record.header.tenant_id;
let record_lsn = record.header.lsn;
let Ok((node_id, labels)) = zerompk::from_msgpack::<(String, Vec<String>)>(&record.payload)
else {
warn!(
core = self.core_id,
lsn = record_lsn,
"WAL graph node-label replay: malformed payload; skipping"
);
return Some(0);
};
let partition = self.csr_partition_mut(database_id.as_u64(), tenant_id);
if is_set {
for label in &labels {
if let Err(e) = partition.add_node_label(&node_id, label) {
warn!(
core = self.core_id,
%node_id,
lsn = record_lsn,
error = %e,
"WAL graph node-label replay: set label failed; skipping"
);
return Some(0);
}
}
} else {
for label in &labels {
partition.remove_node_label(&node_id, label);
}
}
Some(1)
}
pub(crate) fn replay_graph_node_label_wal(&mut self, records: &[WalRecord], num_cores: usize) {
let mut replayed = 0usize;
for record in records {
let vshard_id = record.header.vshard_id as usize;
let target_core = if num_cores > 0 {
vshard_id % num_cores
} else {
0
};
if target_core != self.core_id {
continue;
}
let database_id = DatabaseId::new(record.header.database_id);
if let Some(applied) = self.try_replay_graph_node_label(record, database_id) {
replayed += applied;
}
}
if replayed > 0 {
tracing::info!(
core = self.core_id,
replayed,
"WAL graph node-label replay complete"
);
}
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use nodedb_wal::WalRecord;
use nodedb_wal::WalRecordArgs;
use nodedb_wal::record::RecordType;
use super::CoreLoop;
use crate::bridge::envelope::PhysicalPlan;
use crate::control::server::wal_dispatch::wal_append_if_write;
use crate::types::{DatabaseId, TenantId, VShardId};
use crate::wal::manager::WalManager;
use nodedb_physical::physical_plan::GraphOp;
const TID: u64 = 7;
struct CoreHarness {
core: CoreLoop,
_req_tx: nodedb_bridge::buffer::Producer<crate::bridge::dispatch::BridgeRequest>,
_resp_rx: nodedb_bridge::buffer::Consumer<crate::bridge::dispatch::BridgeResponse>,
_dir: tempfile::TempDir,
}
fn make_core() -> CoreHarness {
use crate::bridge::dispatch::{BridgeRequest, BridgeResponse};
use nodedb_bridge::buffer::RingBuffer;
let dir = tempfile::tempdir().expect("tempdir");
let (req_tx, req_rx) = RingBuffer::channel::<BridgeRequest>(64);
let (resp_tx, resp_rx) = RingBuffer::channel::<BridgeResponse>(64);
let core = CoreLoop::open(
0,
req_rx,
resp_tx,
dir.path(),
Arc::new(nodedb_types::OrdinalClock::new()),
)
.expect("open core");
CoreHarness {
core,
_req_tx: req_tx,
_resp_rx: resp_rx,
_dir: dir,
}
}
fn has_label(core: &CoreLoop, node_id: &str, label: &str) -> bool {
let Some(partition) = core.csr_partition(DatabaseId::DEFAULT.as_u64(), TID) else {
return false;
};
let Some(id) = partition.node_id(node_id) else {
return false;
};
partition.node_has_label(id.raw(partition.partition_tag()), label)
}
#[test]
fn autocommit_set_node_labels_produces_durable_lsn() {
let dir = tempfile::tempdir().expect("wal tempdir");
let wal = WalManager::open_for_testing(&dir.path().join("wal")).expect("open wal");
let plan = PhysicalPlan::Graph(GraphOp::SetNodeLabels {
node_id: "alice".to_string(),
labels: vec!["Person".to_string()],
});
let outcome = wal_append_if_write(
&wal,
TenantId::new(TID),
VShardId::new(0),
DatabaseId::DEFAULT,
&plan,
)
.expect("wal append set node labels");
assert!(
outcome.lsn.is_some(),
"autocommit GraphOp::SetNodeLabels must be durably WAL-appended \
(pre-fix: it fell through the catch-all and was never logged)"
);
}
#[test]
fn autocommit_remove_node_labels_produces_durable_lsn() {
let dir = tempfile::tempdir().expect("wal tempdir");
let wal = WalManager::open_for_testing(&dir.path().join("wal")).expect("open wal");
let plan = PhysicalPlan::Graph(GraphOp::RemoveNodeLabels {
node_id: "alice".to_string(),
labels: vec!["Person".to_string()],
});
let outcome = wal_append_if_write(
&wal,
TenantId::new(TID),
VShardId::new(0),
DatabaseId::DEFAULT,
&plan,
)
.expect("wal append remove node labels");
assert!(
outcome.lsn.is_some(),
"autocommit GraphOp::RemoveNodeLabels must be durably WAL-appended \
(pre-fix: it fell through the catch-all and was never logged)"
);
}
#[test]
fn replay_from_empty_sets_label_on_existing_node() {
let dir = tempfile::tempdir().expect("wal tempdir");
let wal = WalManager::open_for_testing(&dir.path().join("wal")).expect("open wal");
let plan = PhysicalPlan::Graph(GraphOp::SetNodeLabels {
node_id: "alice".to_string(),
labels: vec!["Person".to_string()],
});
wal_append_if_write(
&wal,
TenantId::new(TID),
VShardId::new(0),
DatabaseId::DEFAULT,
&plan,
)
.expect("wal append");
wal.sync().expect("wal sync");
let records = wal.replay().expect("wal replay read");
let mut h = make_core();
h.core
.csr_partition_mut(DatabaseId::DEFAULT.as_u64(), TID)
.add_node("alice")
.expect("seed node");
h.core.replay_graph_node_label_wal(&records, 1);
assert!(
has_label(&h.core, "alice", "Person"),
"label must be present after replay from empty \
(pre-fix: SetNodeLabels was never WAL-logged, so nothing to replay)"
);
}
#[test]
fn remove_after_set_leaves_label_absent_after_replay() {
let dir = tempfile::tempdir().expect("wal tempdir");
let wal = WalManager::open_for_testing(&dir.path().join("wal")).expect("open wal");
let set_plan = PhysicalPlan::Graph(GraphOp::SetNodeLabels {
node_id: "alice".to_string(),
labels: vec!["Person".to_string()],
});
let remove_plan = PhysicalPlan::Graph(GraphOp::RemoveNodeLabels {
node_id: "alice".to_string(),
labels: vec!["Person".to_string()],
});
for plan in [&set_plan, &remove_plan] {
wal_append_if_write(
&wal,
TenantId::new(TID),
VShardId::new(0),
DatabaseId::DEFAULT,
plan,
)
.expect("wal append");
}
wal.sync().expect("wal sync");
let records = wal.replay().expect("wal replay read");
let mut h = make_core();
h.core
.csr_partition_mut(DatabaseId::DEFAULT.as_u64(), TID)
.add_node("alice")
.expect("seed node");
h.core.replay_graph_node_label_wal(&records, 1);
assert!(
!has_label(&h.core, "alice", "Person"),
"label must be absent after Set-then-Remove replay \
(proves ordering and that Remove is logged too)"
);
}
#[test]
fn label_on_edgeless_node_survives_replay() {
let dir = tempfile::tempdir().expect("wal tempdir");
let wal = WalManager::open_for_testing(&dir.path().join("wal")).expect("open wal");
let plan = PhysicalPlan::Graph(GraphOp::SetNodeLabels {
node_id: "ghost".to_string(),
labels: vec!["Person".to_string()],
});
wal_append_if_write(
&wal,
TenantId::new(TID),
VShardId::new(0),
DatabaseId::DEFAULT,
&plan,
)
.expect("wal append");
wal.sync().expect("wal sync");
let records = wal.replay().expect("wal replay read");
let mut h = make_core();
h.core.replay_graph_node_label_wal(&records, 1);
let partition = h.core.csr_partition(DatabaseId::DEFAULT.as_u64(), TID);
assert!(
partition.is_some_and(|p| p.contains_node("ghost")),
"replaying a label on a never-edged node must vivify the node, \
matching the live handler"
);
assert!(
has_label(&h.core, "ghost", "Person"),
"the label itself must be set after replay, not just the node created"
);
}
#[test]
fn malformed_payload_does_not_panic() {
let record = WalRecord::new(WalRecordArgs {
record_type: RecordType::GraphNodeLabelSet as u32,
lsn: 1,
tenant_id: TID,
vshard_id: 0,
database_id: 0,
payload: vec![0xff, 0xff, 0xff],
encryption_key: None,
preamble_bytes: None,
})
.expect("wal record");
let mut h = make_core();
h.core
.replay_graph_node_label_wal(std::slice::from_ref(&record), 1);
}
}