use crate::error::Result;
use crate::transaction::TransactionId;
use crate::types::{Edge, Node, NodeId, EdgeId};
use crate::wal::WalRecord;
const MAX_BATCH: usize = 16;
const CHANNEL_CAPACITY: usize = 256;
#[derive(Debug)]
pub(crate) enum WriteOp {
AddNode { node: Node, skip_indexing: bool },
AddEdgeAt { edge: Edge, timestamp: u64 },
DeleteNode { id: NodeId },
DeleteEdgeAt { id: EdgeId, timestamp: u64 },
AddPropertyIndexEntry {
property: String,
value: crate::types::PropertyValue,
node_id: NodeId,
},
RemovePropertyIndexEntry {
property: String,
value: crate::types::PropertyValue,
node_id: NodeId,
},
}
#[derive(Debug)]
pub(crate) struct CommitSet {
pub tx_id: TransactionId,
pub begin_timestamp: u64,
pub deleted_nodes: Vec<(NodeId, Node)>,
pub deleted_edges: Vec<EdgeId>,
pub pending_nodes: Vec<Node>,
pub pending_edges: Vec<Edge>,
}
impl CommitSet {
pub(crate) fn wal_records(&self, commit_timestamp: u64) -> Vec<WalRecord> {
let mut records = Vec::with_capacity(
2 + self.deleted_nodes.len() + self.pending_nodes.len() + self.pending_edges.len(),
);
records.push(WalRecord::Begin {
tx_id: self.tx_id,
timestamp: self.begin_timestamp,
});
for (node_id, node) in &self.deleted_nodes {
records.push(WalRecord::DeleteNode {
tx_id: self.tx_id,
node_id: *node_id,
node: node.clone(),
});
}
for node in &self.pending_nodes {
records.push(WalRecord::InsertNode {
tx_id: self.tx_id,
node: node.clone(),
});
}
for edge in &self.pending_edges {
records.push(WalRecord::InsertEdge {
tx_id: self.tx_id,
edge: edge.clone(),
});
}
records.push(WalRecord::Commit {
tx_id: self.tx_id,
timestamp: commit_timestamp,
});
records
}
}
pub(crate) enum Work {
Op(WriteOp),
Commit(CommitSet),
}
pub(crate) struct ApplierMsg {
pub graph: super::Graph,
pub work: Work,
pub ack: tokio::sync::oneshot::Sender<Result<()>>,
}
pub(crate) fn spawn_applier() -> tokio::sync::mpsc::Sender<ApplierMsg> {
let (tx, mut rx) = tokio::sync::mpsc::channel::<ApplierMsg>(CHANNEL_CAPACITY);
tokio::spawn(async move {
while let Some(first) = rx.recv().await {
let mut batch = vec![first];
while batch.len() < MAX_BATCH {
match rx.try_recv() {
Ok(msg) => batch.push(msg),
Err(_) => break,
}
}
process_batch(batch).await;
}
log::debug!("Write applier task exited (all graph handles dropped)");
});
tx
}
async fn process_batch(batch: Vec<ApplierMsg>) {
let anchor = batch[0].graph.clone();
let gate = anchor.write_gate();
let _gate = gate.lock().await;
let mut commit_timestamps: Vec<Option<u64>> = Vec::with_capacity(batch.len());
let mut group_records: Vec<WalRecord> = Vec::new();
let mut commits_in_group = 0usize;
for msg in &batch {
match &msg.work {
Work::Commit(set) => {
let ts = msg.graph.next_logical_timestamp();
group_records.extend(set.wal_records(ts));
commit_timestamps.push(Some(ts));
commits_in_group += 1;
}
Work::Op(_) => commit_timestamps.push(None),
}
}
let wal_ok = if group_records.is_empty() {
Ok(())
} else {
anchor.wal().append_batch(&group_records).await.map(|_| ())
};
if commits_in_group > 1 {
log::debug!(
"Group commit: {} transactions, {} WAL records, 1 fsync",
commits_in_group,
group_records.len()
);
}
for (msg, commit_ts) in batch.into_iter().zip(commit_timestamps) {
let result = match (&wal_ok, msg.work) {
(Err(e), Work::Commit(_)) => Err(crate::error::NopalError::custom(format!(
"group WAL fsync failed, commit aborted before apply: {}",
e
))),
(_, Work::Op(op)) => msg.graph.apply_write_op(op).await,
(Ok(()), Work::Commit(set)) => {
msg.graph
.apply_commit_set(&set, commit_ts.expect("commit has timestamp"))
.await
}
};
let _ = msg.ack.send(result);
}
if let Err(e) = anchor.persist_clocks().await {
log::warn!("applier: failed to persist logical clocks after batch: {}", e);
}
}