use crate::counters::AccessStats;
use crate::error::TopoError;
use crate::feed::ChangeEvent;
use crate::ids::{EdgeId, NodeId, Scope, ScopeSet};
use crate::index::IndexSpec;
use crate::op::Op;
use crate::state::NodeRecord;
use crate::storage::{AppliedBatch, Storage};
use crossbeam_channel::{bounded, Receiver, Sender};
use std::path::Path;
use std::sync::{Arc, Mutex};
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, Copy, Default)]
pub struct DbOptions {
pub cache_size_bytes: Option<usize>,
}
type ApplyJob = (Vec<Op>, Sender<Result<AppliedBatch, TopoError>>);
enum Job {
Apply {
ops: Vec<Op>,
at: Option<i64>,
reply: Sender<Result<AppliedBatch, TopoError>>,
},
Rebuild {
reply: Sender<Result<(), TopoError>>,
},
BumpCounters { bumps: Vec<(NodeId, u64, i64)> },
Compact {
keep_from: u64,
reply: Sender<Result<(), TopoError>>,
},
}
#[derive(Clone)]
pub struct Db {
inner: Arc<Inner>,
}
impl std::fmt::Debug for Db {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Db").finish_non_exhaustive()
}
}
struct Inner {
storage: Arc<Storage>,
tx: Mutex<Option<Sender<Job>>>,
applier: Mutex<Option<std::thread::JoinHandle<()>>>,
bump_tx: Mutex<Option<Sender<(NodeId, i64)>>>,
bumper: Mutex<Option<std::thread::JoinHandle<()>>>,
subs: Arc<Mutex<Vec<Sender<ChangeEvent>>>>,
}
impl Db {
pub fn open(path: impl AsRef<Path>) -> Result<Self, TopoError> {
Self::open_with(path, IndexSpec::default())
}
pub fn open_stored(path: impl AsRef<Path>) -> Result<Self, TopoError> {
let path = path.as_ref();
let spec = Storage::read_persisted_index_spec(path)?.unwrap_or_default();
Self::open_with(path, spec)
}
pub fn open_with(path: impl AsRef<Path>, spec: IndexSpec) -> Result<Self, TopoError> {
Self::open_with_options(path, spec, DbOptions::default())
}
pub fn open_with_options(
path: impl AsRef<Path>,
spec: IndexSpec,
options: DbOptions,
) -> Result<Self, TopoError> {
spec.validate()?;
let spec = Arc::new(spec);
let storage = Arc::new(Storage::open_with_options(path, spec, options)?);
let (tx, rx) = bounded::<Job>(256);
let storage_for_applier = storage.clone();
let subs: Arc<Mutex<Vec<Sender<ChangeEvent>>>> = Arc::new(Mutex::new(Vec::new()));
let subs_for_applier = subs.clone();
let applier = std::thread::spawn(move || {
let mut pending: Option<Job> = None;
loop {
let job = match pending.take() {
Some(job) => job,
None => match rx.recv() {
Ok(job) => job,
Err(_) => break,
},
};
match job {
Job::Apply { ops, at, reply } => {
let Some(now) = at else {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock before UNIX epoch")
.as_millis() as i64;
let mut jobs: Vec<ApplyJob> = vec![(ops, reply)];
let mut total_ops = jobs[0].0.len();
while jobs.len() < 16 && total_ops < 4096 {
match rx.try_recv() {
Ok(Job::Apply {
ops,
at: None,
reply,
}) => {
total_ops += ops.len();
jobs.push((ops, reply));
}
Ok(other) => {
pending = Some(other);
break;
}
Err(_) => break,
}
}
if jobs.len() == 1 {
let (ops, reply) = jobs.pop().expect("len checked above");
apply_one_job(
&storage_for_applier,
&subs_for_applier,
ops,
now,
reply,
);
} else {
apply_group(&storage_for_applier, &subs_for_applier, jobs, now);
}
continue;
};
apply_one_job(&storage_for_applier, &subs_for_applier, ops, now, reply);
}
Job::Rebuild { reply } => {
let result = storage_for_applier.rebuild_state_from_ops();
let _ = reply.send(result);
}
Job::BumpCounters { bumps } => {
let _ = storage_for_applier.merge_counter_bumps(&bumps);
}
Job::Compact { keep_from, reply } => {
let _ = reply.send(storage_for_applier.compact_ops_through(keep_from));
}
}
}
});
let (bump_tx, bump_rx) = bounded::<(NodeId, i64)>(4096);
let applier_tx_for_bumper = tx.clone();
let bumper = std::thread::spawn(move || {
let mut pending: std::collections::HashMap<NodeId, (u64, i64)> = Default::default();
let flush = |pending: &mut std::collections::HashMap<NodeId, (u64, i64)>| {
if pending.is_empty() {
return;
}
let bumps: Vec<(NodeId, u64, i64)> =
pending.drain().map(|(id, (n, ts))| (id, n, ts)).collect();
let _ = applier_tx_for_bumper.send(Job::BumpCounters { bumps });
};
loop {
match bump_rx.recv_timeout(std::time::Duration::from_millis(100)) {
Ok((id, ts)) => {
let e = pending.entry(id).or_insert((0, 0));
e.0 += 1;
e.1 = e.1.max(ts);
if pending.len() >= 256 {
flush(&mut pending);
}
}
Err(crossbeam_channel::RecvTimeoutError::Timeout) => flush(&mut pending),
Err(crossbeam_channel::RecvTimeoutError::Disconnected) => {
flush(&mut pending);
break;
}
}
}
});
Ok(Self {
inner: Arc::new(Inner {
storage,
tx: Mutex::new(Some(tx)),
applier: Mutex::new(Some(applier)),
subs,
bump_tx: Mutex::new(Some(bump_tx)),
bumper: Mutex::new(Some(bumper)),
}),
})
}
#[must_use]
pub(crate) fn storage(&self) -> &Storage {
&self.inner.storage
}
pub fn format_version(&self) -> u32 {
self.inner
.storage
.format_version()
.expect("format_version: META row guaranteed by a successful open")
}
#[must_use]
pub fn index_spec(&self) -> IndexSpec {
(*self.inner.storage.spec).clone()
}
#[doc(hidden)]
pub fn storage_report(&self) -> Result<Vec<crate::storage::TableReport>, TopoError> {
self.inner.storage.storage_report()
}
pub(crate) fn bump(&self, ids: impl IntoIterator<Item = NodeId>) {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock before UNIX epoch")
.as_millis() as i64;
let tx = self
.inner
.bump_tx
.lock()
.ok()
.and_then(|g| g.as_ref().cloned());
if let Some(tx) = tx {
for id in ids {
let _ = tx.try_send((id, now));
}
}
}
pub fn access_stats(
&self,
scopes: &ScopeSet,
id: NodeId,
) -> Result<Option<AccessStats>, TopoError> {
let in_scope = self
.inner
.storage
.load_node(id)?
.is_some_and(|n| scopes.contains(n.scope));
if !in_scope {
return Ok(None);
}
Ok(Some(
self.inner.storage.read_counter(id)?.unwrap_or_default(),
))
}
pub(crate) fn access_count_unbumped(&self, id: NodeId) -> u64 {
self.inner
.storage
.read_counter(id)
.ok()
.flatten()
.map(|s| s.access_count)
.unwrap_or(0)
}
pub fn submit(&self, ops: Vec<Op>) -> Result<AppliedBatch, TopoError> {
self.submit_inner(ops, None)
}
pub fn submit_at(&self, ops: Vec<Op>, now_ms: i64) -> Result<AppliedBatch, TopoError> {
self.submit_inner(ops, Some(now_ms))
}
#[must_use]
pub fn subscribe(&self, capacity: usize) -> Receiver<ChangeEvent> {
let capacity = capacity.max(1);
let (tx, rx) = bounded::<ChangeEvent>(capacity);
match self.inner.subs.lock() {
Ok(mut subs) => subs.push(tx),
Err(_) => {
let (tx, rx) = bounded(1);
drop(tx);
return rx;
}
}
rx
}
pub fn ops_since(&self, since_seq: u64) -> Result<Vec<ChangeEvent>, TopoError> {
let ops = self.inner.storage.read_ops(since_seq)?;
Ok(ops
.into_iter()
.map(|(seq, op)| ChangeEvent {
seq,
op: Arc::new(op),
})
.collect())
}
#[must_use = "the seq anchors ops_since"]
pub fn current_seq(&self) -> Result<u64, TopoError> {
self.inner.storage.current_seq()
}
pub fn compact_ops(&self, keep_from: u64) -> Result<(), TopoError> {
let (reply_tx, reply_rx) = bounded(1);
let tx = self.sender().ok_or(TopoError::Closed)?;
tx.send(Job::Compact {
keep_from,
reply: reply_tx,
})
.map_err(|_| TopoError::Closed)?;
reply_rx.recv().map_err(|_| TopoError::Closed)?
}
fn sender(&self) -> Option<Sender<Job>> {
self.inner.tx.lock().ok().and_then(|g| g.as_ref().cloned())
}
fn submit_inner(&self, ops: Vec<Op>, at: Option<i64>) -> Result<AppliedBatch, TopoError> {
let (reply_tx, reply_rx) = bounded(1);
let tx = self.sender().ok_or(TopoError::Closed)?;
tx.send(Job::Apply {
ops,
at,
reply: reply_tx,
})
.map_err(|_| TopoError::Closed)?;
reply_rx.recv().map_err(|_| TopoError::Closed)?
}
#[doc(hidden)]
pub fn all_edges_between(&self, from: NodeId, to: NodeId) -> Vec<crate::state::EdgeRecord> {
self.edges_between(from, to).unwrap_or_default()
}
#[doc(hidden)]
pub fn open_edges_between(&self, from: NodeId, to: NodeId) -> Vec<EdgeId> {
self.edges_between(from, to)
.unwrap_or_default()
.into_iter()
.filter(|e| e.valid_to.is_none())
.map(|e| e.id)
.collect()
}
pub fn edges_from(
&self,
scopes: &ScopeSet,
from: NodeId,
to: Option<NodeId>,
ty: Option<&str>,
open_only: bool,
) -> Result<Vec<crate::state::EdgeRecord>, TopoError> {
let storage = self.storage();
let dicts = storage.dicts.read().expect("dict lock poisoned");
let scope_registry = storage
.scope_registry
.read()
.expect("scope registry lock poisoned");
let type_filter: Option<Vec<u32>> = ty.map(|name| {
dicts
.id_of(crate::dict::DictKind::EdgeType, name)
.into_iter()
.collect()
});
let tx = storage.db.begin_read().map_err(crate::error::storage_err)?;
let node_slots = tx
.open_table(crate::slots::NODE_SLOTS)
.map_err(crate::error::storage_err)?;
let Some(from_slot) = crate::slots::node_slot(&node_slots, from)? else {
return Ok(Vec::new());
};
let to_slot = match to {
None => None,
Some(to) => match crate::slots::node_slot(&node_slots, to)? {
None => return Ok(Vec::new()),
some => some,
},
};
let out_adj = tx
.open_table(crate::adj::OUT_ADJ)
.map_err(crate::error::storage_err)?;
let edges_table = tx
.open_table(crate::storage::EDGES)
.map_err(crate::error::storage_err)?;
let node_ids = tx
.open_table(crate::slots::NODE_IDS)
.map_err(crate::error::storage_err)?;
let mut out = Vec::new();
for (_ty, entry) in crate::adj::read_adj(&out_adj, from_slot, type_filter.as_deref())? {
if to_slot.is_some_and(|slot| entry.target != slot) {
continue;
}
if open_only && entry.valid_to.is_some() {
continue;
}
let entry_scope = scope_registry.resolve(entry.scope)?;
if !scopes.contains(entry_scope) {
continue;
}
if let Some(rec) = crate::storage::read_edge_by_slot(
&edges_table,
&dicts,
&scope_registry,
&node_ids,
entry.edge,
)? {
out.push(rec);
}
}
out.sort_by_key(|e| e.id);
Ok(out)
}
fn edges_between(
&self,
from: NodeId,
to: NodeId,
) -> Result<Vec<crate::state::EdgeRecord>, TopoError> {
let storage = self.storage();
let dicts = storage.dicts.read().expect("dict lock poisoned");
let scope_registry = storage
.scope_registry
.read()
.expect("scope registry lock poisoned");
let tx = storage.db.begin_read().map_err(crate::error::storage_err)?;
let node_slots = tx
.open_table(crate::slots::NODE_SLOTS)
.map_err(crate::error::storage_err)?;
let Some(from_slot) = crate::slots::node_slot(&node_slots, from)? else {
return Ok(Vec::new());
};
let Some(to_slot) = crate::slots::node_slot(&node_slots, to)? else {
return Ok(Vec::new());
};
let out_adj = tx
.open_table(crate::adj::OUT_ADJ)
.map_err(crate::error::storage_err)?;
let edges_table = tx
.open_table(crate::storage::EDGES)
.map_err(crate::error::storage_err)?;
let node_ids = tx
.open_table(crate::slots::NODE_IDS)
.map_err(crate::error::storage_err)?;
let mut out = Vec::new();
for (_ty, entry) in crate::adj::read_adj(&out_adj, from_slot, None)? {
if entry.target != to_slot {
continue;
}
if let Some(rec) = crate::storage::read_edge_by_slot(
&edges_table,
&dicts,
&scope_registry,
&node_ids,
entry.edge,
)? {
out.push(rec);
}
}
Ok(out)
}
#[doc(hidden)]
pub fn rebuild_state_from_ops(&self) -> Result<(), TopoError> {
let (reply_tx, reply_rx) = bounded(1);
let tx = self.sender().ok_or(TopoError::Closed)?;
tx.send(Job::Rebuild { reply: reply_tx })
.map_err(|_| TopoError::Closed)?;
reply_rx.recv().map_err(|_| TopoError::Closed)?
}
#[doc(hidden)]
pub fn debug_dump_nodes(&self) -> Vec<crate::state::NodeRecord> {
let mut out = self
.inner
.storage
.all_nodes()
.expect("debug dump: storage read failed");
out.sort_by_key(|n| n.id);
out
}
#[doc(hidden)]
pub fn debug_dump_edges(&self) -> Vec<crate::state::EdgeRecord> {
let mut out = self
.inner
.storage
.all_edges()
.expect("debug dump: storage read failed");
out.sort_by_key(|e| e.id);
out
}
#[doc(hidden)]
pub fn debug_dump_adjacency(&self) -> Result<Vec<AdjacencyDumpRow>, TopoError> {
use redb::ReadableTable;
let storage = self.storage();
let tx = storage.db.begin_read().map_err(crate::error::storage_err)?;
let mut out = Vec::new();
for (is_out, table_def) in [(true, crate::adj::OUT_ADJ), (false, crate::adj::IN_ADJ)] {
let table = tx
.open_table(table_def)
.map_err(crate::error::storage_err)?;
for entry in table.iter().map_err(crate::error::storage_err)? {
let (k, v) = entry.map_err(crate::error::storage_err)?;
let key: [u8; 16] = k
.value()
.try_into()
.map_err(|_| TopoError::Encoding("bad adjacency key".into()))?;
let slot = u64::from_be_bytes(key[..8].try_into().expect("8-byte slice"));
let edge_type = u32::from_be_bytes(key[8..12].try_into().expect("4-byte slice"));
let raw = crate::codec::unframe_value(v.value())?;
for e in crate::adj::decode_block(raw.as_ref())? {
out.push((
slot,
edge_type,
is_out,
e.target,
e.edge,
e.scope,
e.valid_from,
e.valid_to,
));
}
}
}
out.sort_unstable();
Ok(out)
}
#[doc(hidden)]
pub fn debug_dump_postings(&self) -> Result<Vec<PostingsDumpRow>, TopoError> {
use redb::ReadableTable;
let storage = self.storage();
let tx = storage.db.begin_read().map_err(crate::error::storage_err)?;
let table = tx
.open_table(crate::storage::POSTINGS)
.map_err(crate::error::storage_err)?;
let mut out = Vec::new();
for entry in table.iter().map_err(crate::error::storage_err)? {
let (k, v) = entry.map_err(crate::error::storage_err)?;
let key = k.value().to_vec();
let raw = crate::codec::unframe_value(v.value())?;
let entries = crate::fts::decode_posting_block(raw.as_ref())?;
out.push((key, entries));
}
out.sort_unstable();
Ok(out)
}
#[doc(hidden)]
pub fn debug_dump_vectors(&self) -> Result<Vec<VectorsDumpRow>, TopoError> {
use redb::ReadableTable;
let storage = self.storage();
let tx = storage.db.begin_read().map_err(crate::error::storage_err)?;
let table = tx
.open_table(crate::vector_store::VECTORS)
.map_err(crate::error::storage_err)?;
let mut out = Vec::new();
for entry in table.iter().map_err(crate::error::storage_err)? {
let (k, v) = entry.map_err(crate::error::storage_err)?;
let key: [u8; 16] = k
.value()
.try_into()
.map_err(|_| TopoError::Encoding("bad vectors key".into()))?;
let model = u32::from_be_bytes(key[0..4].try_into().expect("4-byte slice"));
let scope = u32::from_be_bytes(key[4..8].try_into().expect("4-byte slice"));
let slot = u64::from_be_bytes(key[8..16].try_into().expect("8-byte slice"));
let raw = crate::codec::unframe_value(v.value())?;
let vector: Vec<f32> = postcard::from_bytes(raw.as_ref())
.map_err(|e| TopoError::Encoding(e.to_string()))?;
out.push((model, scope, slot, vector));
}
out.sort_by_key(|a| (a.0, a.1, a.2));
Ok(out)
}
#[doc(hidden)]
pub fn debug_dump_embedding_ref(&self) -> Result<Vec<EmbeddingRefDumpRow>, TopoError> {
use redb::ReadableTable;
let storage = self.storage();
let tx = storage.db.begin_read().map_err(crate::error::storage_err)?;
let table = tx
.open_table(crate::vector_store::EMBEDDING_REF)
.map_err(crate::error::storage_err)?;
let mut out = Vec::new();
for entry in table.iter().map_err(crate::error::storage_err)? {
let (k, v) = entry.map_err(crate::error::storage_err)?;
let key: [u8; 8] = k
.value()
.try_into()
.map_err(|_| TopoError::Encoding("bad embedding_ref key".into()))?;
let slot = u64::from_be_bytes(key);
let (model, scope) = crate::vector_store::decode_ref(v.value())?;
out.push((slot, model, scope));
}
out.sort_unstable();
Ok(out)
}
#[doc(hidden)]
pub fn debug_dump_vector_dims(&self) -> Result<Vec<VectorDimsDumpRow>, TopoError> {
use redb::ReadableTable;
let storage = self.storage();
let tx = storage.db.begin_read().map_err(crate::error::storage_err)?;
let table = tx
.open_table(crate::storage::VECTOR_DIMS)
.map_err(crate::error::storage_err)?;
let mut out = Vec::new();
for entry in table.iter().map_err(crate::error::storage_err)? {
let (k, v) = entry.map_err(crate::error::storage_err)?;
let key: [u8; 4] = k
.value()
.try_into()
.map_err(|_| TopoError::Encoding("bad vector_dims key".into()))?;
let model_id = u32::from_be_bytes(key);
let val: [u8; 4] = v
.value()
.try_into()
.map_err(|_| TopoError::Encoding("bad vector_dims value".into()))?;
let dim = u32::from_le_bytes(val);
out.push((model_id, dim));
}
out.sort_unstable();
Ok(out)
}
#[doc(hidden)]
pub fn debug_dump_label_index(&self) -> Result<Vec<LabelIndexDumpRow>, TopoError> {
use redb::ReadableTable;
let storage = self.storage();
let tx = storage.db.begin_read().map_err(crate::error::storage_err)?;
let table = tx
.open_table(crate::storage::LABEL_INDEX)
.map_err(crate::error::storage_err)?;
let mut out = Vec::new();
for entry in table.iter().map_err(crate::error::storage_err)? {
let (k, v) = entry.map_err(crate::error::storage_err)?;
let key: [u8; 24] = k
.value()
.try_into()
.map_err(|_| TopoError::Encoding("bad label_index key".into()))?;
let label_id = u32::from_be_bytes(key[0..4].try_into().expect("4-byte slice"));
let scope_id = u32::from_be_bytes(key[4..8].try_into().expect("4-byte slice"));
let node_id = u128::from_be_bytes(key[8..24].try_into().expect("16-byte slice"));
out.push((label_id, scope_id, node_id, v.value()));
}
out.sort_unstable();
Ok(out)
}
}
#[doc(hidden)]
pub type AdjacencyDumpRow = (u64, u32, bool, u64, u64, u32, i64, Option<i64>);
#[doc(hidden)]
pub type PostingsDumpRow = (Vec<u8>, Vec<(u64, u32)>);
#[doc(hidden)]
pub type VectorsDumpRow = (u32, u32, u64, Vec<f32>);
#[doc(hidden)]
pub type EmbeddingRefDumpRow = (u64, u32, u32);
#[doc(hidden)]
pub type VectorDimsDumpRow = (u32, u32);
#[doc(hidden)]
pub type LabelIndexDumpRow = (u32, u32, u128, u64);
fn ids_needing_pre_state(ops: &[Op]) -> std::collections::HashSet<NodeId> {
let mut ids = std::collections::HashSet::new();
for op in ops {
match op {
Op::CreateEdge { from, to, .. } => {
ids.insert(*from);
ids.insert(*to);
}
Op::CreateNode { id, .. } => {
ids.insert(*id);
}
_ => {}
}
}
ids
}
fn group_pre_read_error(e: &TopoError) -> TopoError {
let msg = e.to_string();
let ctx = |m: String| format!("group pre-validation read failed: {m}");
match e {
TopoError::Storage(_) => TopoError::Storage(Box::new(redb::Error::Corrupted(ctx(msg)))),
TopoError::Busy => TopoError::Busy,
TopoError::Encoding(m) => TopoError::Encoding(ctx(m.clone())),
TopoError::Rejected(m) => TopoError::Rejected(ctx(m.clone())),
TopoError::Compacted { oldest } => TopoError::Compacted { oldest: *oldest },
TopoError::Closed => TopoError::Closed,
TopoError::UnsupportedFormat { found, supported } => TopoError::UnsupportedFormat {
found: *found,
supported: *supported,
},
}
}
fn apply_one_job(
storage: &Storage,
subs: &Arc<Mutex<Vec<Sender<ChangeEvent>>>>,
ops: Vec<Op>,
now: i64,
reply: Sender<Result<AppliedBatch, TopoError>>,
) {
let pre = match storage.load_nodes(&ids_needing_pre_state(&ops)) {
Ok(m) => m,
Err(e) => {
let _ = reply.send(Err(e));
return;
}
};
if let Err(e) = crate::validate::prevalidate_edge_scopes(&pre, &ops) {
let _ = reply.send(Err(e));
return;
}
if let Err(e) = crate::validate::prevalidate_create_node_ids(&pre, &ops) {
let _ = reply.send(Err(e));
return;
}
match storage.apply_batch(ops, now) {
Ok(batch) => {
broadcast_batch(subs, &batch);
let _ = reply.send(Ok(batch));
}
Err(e) => {
let _ = reply.send(Err(e));
}
}
}
fn broadcast_batch(subs: &Arc<Mutex<Vec<Sender<ChangeEvent>>>>, batch: &AppliedBatch) {
let ev_ops: Vec<Arc<Op>> = batch
.resolved
.iter()
.map(|op| Arc::new(op.clone()))
.collect();
let mut subs = subs.lock().unwrap();
subs.retain(|s| {
for (i, ev_op) in ev_ops.iter().enumerate() {
let ev = ChangeEvent {
seq: batch.first_seq + i as u64,
op: ev_op.clone(),
};
match s.try_send(ev) {
Ok(()) => {}
Err(crossbeam_channel::TrySendError::Full(_)) => {}
Err(crossbeam_channel::TrySendError::Disconnected(_)) => return false,
}
}
true
});
}
fn apply_group(
storage: &Storage,
subs: &Arc<Mutex<Vec<Sender<ChangeEvent>>>>,
jobs: Vec<ApplyJob>,
now: i64,
) {
let mut all_ids = std::collections::HashSet::new();
for (ops, _) in &jobs {
all_ids.extend(ids_needing_pre_state(ops));
}
let base_pre = match storage.load_nodes(&all_ids) {
Ok(m) => m,
Err(e) => {
for (_, reply) in jobs {
let _ = reply.send(Err(group_pre_read_error(&e)));
}
return;
}
};
let mut overlay: std::collections::HashMap<NodeId, Option<Scope>> =
std::collections::HashMap::new();
let mut included: Vec<ApplyJob> = Vec::with_capacity(jobs.len());
for (ops, reply) in jobs {
let mut effective_pre = base_pre.clone();
for (&id, ov) in &overlay {
match ov {
Some(scope) => {
effective_pre.insert(
id,
NodeRecord {
id,
scope: *scope,
label: Default::default(),
props: Default::default(),
embedding: None,
},
);
}
None => {
effective_pre.remove(&id);
}
}
}
if let Err(e) = crate::validate::prevalidate_edge_scopes(&effective_pre, &ops) {
let _ = reply.send(Err(e));
continue;
}
if let Err(e) = crate::validate::prevalidate_create_node_ids(&effective_pre, &ops) {
let _ = reply.send(Err(e));
continue;
}
for op in &ops {
match op {
Op::CreateNode { id, scope, .. } => {
overlay.insert(*id, Some(*scope));
}
Op::RemoveNode { id } => {
overlay.insert(*id, None);
}
_ => {}
}
}
included.push((ops, reply));
}
if included.is_empty() {
return;
}
if included.len() == 1 {
let (ops, reply) = included.pop().expect("len checked above");
apply_one_job(storage, subs, ops, now, reply);
return;
}
let groups: Vec<Vec<Op>> = included.iter().map(|(ops, _)| ops.clone()).collect();
let results = storage.apply_batches(groups, now);
if results.iter().all(Result::is_ok) {
for ((_, reply), result) in included.into_iter().zip(results) {
let batch = result.expect("checked all Ok above");
broadcast_batch(subs, &batch);
let _ = reply.send(Ok(batch));
}
} else {
for (ops, reply) in included {
apply_one_job(storage, subs, ops, now, reply);
}
}
}
impl Drop for Inner {
fn drop(&mut self) {
self.bump_tx
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take();
if let Some(h) = self
.bumper
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take()
{
let _ = h.join();
}
self.tx
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take();
if let Some(h) = self
.applier
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take()
{
let _ = h.join();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dropped_receiver_is_pruned_on_next_broadcast() {
let dir = tempfile::tempdir().unwrap();
let db = Db::open(dir.path().join("t.redb")).unwrap();
let rx = db.subscribe(4);
drop(rx);
db.submit(vec![crate::Op::CreateNode {
id: crate::NodeId::new(),
scope: crate::Scope::Id(crate::ScopeId::new()),
label: "M".into(),
props: Default::default(),
}])
.unwrap();
assert_eq!(
db.inner.subs.lock().unwrap().len(),
0,
"disconnected sender must be pruned"
);
}
}