use compact_str::CompactString;
use petgraph::Direction;
use petgraph::visit::EdgeRef;
use crate::error::GraphError;
use crate::graph::change::{ChangeSet, CommitMetadata, CommitStamp};
use crate::graph::definition::{Definition, DefinitionKind};
use crate::graph::handle::EdgeGenerations;
use crate::graph::hooks::{CommitHooks, RollbackReason};
use crate::graph::schema::GraphSchema;
use crate::graph::storage::UnitMark;
use crate::graph::{EdgeHandle, EdgeIndex, TraversalGraph, VertexHandle};
use crate::traversal::memory_budget::MemoryBudget;
use crate::value::{ElementProperty, PropertyMap};
pub(crate) const COMMIT_OPERATION: &str = "commit";
pub(crate) const HOOK_PANICKED: &str = "a panic unwound through the commit (in a commit hook's before_commit); the unit was rolled back";
const KEEP_CAPACITY: usize = 1024;
#[derive(Debug, Default)]
pub(crate) struct UndoLog {
entries: Vec<UndoEntry>,
depth: u32,
pub(crate) hooks: CommitHooks,
pub(crate) last_commit_seq: u64,
pub(crate) last_committed_at: i64,
pub(crate) loading: bool,
changed: bool,
sequences_raised: bool,
pub(crate) metadata: CommitMetadata,
budget: Option<MemoryBudget>,
}
impl UndoLog {
#[inline]
pub(crate) fn is_recording(&self) -> bool {
self.depth > 0
}
#[inline]
pub(crate) fn record(&mut self, entry: UndoEntry) {
if self.depth > 0 {
self.entries.push(entry);
self.changed = true;
}
}
#[inline]
pub(crate) fn mark_sequences_raised(&mut self) {
if self.depth > 0 {
self.sequences_raised = true;
self.changed = true;
}
}
pub(crate) fn entries(&self) -> &[UndoEntry] {
&self.entries
}
#[cfg(test)]
pub(crate) fn len(&self) -> usize {
self.entries.len()
}
fn close(&mut self) {
self.depth = self.depth.saturating_sub(1);
if self.depth == 0 {
self.changed = false;
self.sequences_raised = false;
self.metadata = CommitMetadata::default();
self.budget = None;
if self.entries.capacity() > KEEP_CAPACITY {
self.entries = Vec::new();
} else {
self.entries.clear();
}
}
}
}
#[derive(Debug)]
pub(crate) struct VertexImage {
pub(crate) id: CompactString,
pub(crate) labels: Vec<CompactString>,
pub(crate) properties: PropertyMap,
}
#[derive(Debug)]
pub(crate) struct EdgeImage {
pub(crate) id: CompactString,
pub(crate) label: Option<CompactString>,
pub(crate) properties: Option<PropertyMap>,
pub(crate) source: CompactString,
pub(crate) target: CompactString,
}
pub(crate) type OldProperty = Option<(usize, ElementProperty)>;
#[derive(Debug)]
pub(crate) enum UndoEntry {
AddedVertex { id: CompactString },
AddedEdge { id: CompactString },
DroppedVertex {
vertex: Box<VertexImage>,
edges: Vec<EdgeImage>,
},
DroppedEdge(Box<EdgeImage>),
VertexProperty {
id: CompactString,
key: CompactString,
old: OldProperty,
},
EdgeProperty {
id: CompactString,
key: CompactString,
old: OldProperty,
map_was_none: bool,
},
VertexLabels {
id: CompactString,
old: Vec<CompactString>,
},
EdgeLabel {
id: CompactString,
old: Option<CompactString>,
},
Schema(Box<GraphSchema>),
Definition {
kind: DefinitionKind,
name: CompactString,
old: Option<Box<Definition>>,
},
}
pub(crate) fn now_micros() -> i64 {
web_time::SystemTime::now()
.duration_since(web_time::UNIX_EPOCH)
.map_or(0, |elapsed| {
i64::try_from(elapsed.as_micros()).unwrap_or(i64::MAX)
})
}
pub(crate) fn mark_position(mark: UnitMark) -> usize {
usize::try_from(mark.raw()).unwrap_or(usize::MAX)
}
pub(crate) fn restore_property(properties: &mut PropertyMap, key: &str, old: OldProperty) {
properties.shift_remove(key);
if let Some((position, value)) = old {
let position = position.min(properties.len());
properties
.0
.shift_insert(position, CompactString::from(key), value);
}
}
impl TraversalGraph {
pub(crate) fn begin_undo_unit(&mut self) -> UnitMark {
if self.undo.depth == 0 {
self.undo.budget =
crate::traversal::memory_budget::unit_limit(&self.execution_policy, None)
.map(|limit| MemoryBudget::new(limit).start(&*self));
}
self.undo.depth = self.undo.depth.saturating_add(1);
UnitMark::new(self.undo.entries.len() as u64)
}
pub(crate) fn commit_undo_unit(&mut self, mark: UnitMark) -> Result<(), GraphError> {
if self.undo.depth != 1 || (self.undo.entries.is_empty() && !self.undo.sequences_raised) {
self.undo.close();
return Ok(());
}
if let Some(budget) = self.undo.budget
&& let Err(error) = budget.unit_check(&*self, COMMIT_OPERATION)
{
self.rollback_undo_unit(mark, &RollbackReason::failed(&error));
return Err(error);
}
let Some(commit_seq) = self.undo.last_commit_seq.checked_add(1) else {
let error = GraphError::SequenceOverflow {
sequence: crate::error::COMMIT_SEQUENCE,
};
self.rollback_undo_unit(mark, &RollbackReason::failed(&error));
return Err(error);
};
let committed_at = now_micros().max(self.undo.last_committed_at);
if !self.undo.hooks.is_active() {
self.finish_commit(commit_seq, committed_at);
return Ok(());
}
let asked = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let mutations = self.derive_mutations_since(0);
if mutations.is_empty() && !self.undo.sequences_raised {
return None;
}
let metadata = std::mem::take(&mut self.undo.metadata);
let stamp = CommitStamp {
commit_seq,
committed_at,
node_sequence: self.node_sequence,
edge_sequence: self.edge_sequence,
};
let change_set = ChangeSet::new(stamp, metadata, mutations);
let verdict = self.undo.hooks.before_commit(&change_set);
Some((change_set, verdict))
}));
let (change_set, verdict) = match asked {
Ok(Some(asked)) => asked,
Ok(None) => {
self.finish_commit(commit_seq, committed_at);
return Ok(());
}
Err(payload) => {
self.undo_to(mark);
self.undo.close();
let reason = RollbackReason::failed(&HOOK_PANICKED);
if std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
self.undo.hooks.after_rollback(&reason);
}))
.is_err()
{
tracing::error!("a commit hook panicked in after_rollback after a panic");
}
std::panic::resume_unwind(payload)
}
};
match verdict {
Ok(()) => {
self.finish_commit(commit_seq, committed_at);
self.undo.hooks.after_commit(&change_set);
Ok(())
}
Err(error) => {
self.undo_to(mark);
self.undo.close();
self.undo
.hooks
.after_rollback(&RollbackReason::from_error(&error));
Err(error)
}
}
}
fn finish_commit(&mut self, commit_seq: u64, committed_at: i64) {
self.undo.last_commit_seq = commit_seq;
self.undo.last_committed_at = committed_at;
self.undo.close();
}
pub(crate) fn load_outside_units<R>(&mut self, load: impl FnOnce(&mut Self) -> R) -> R {
let previous = std::mem::replace(&mut self.undo.loading, true);
let result = load(self);
self.undo.loading = previous;
result
}
pub(crate) fn set_schema_recorded(&mut self, schema: GraphSchema) -> Result<(), GraphError> {
let old = self
.undo
.is_recording()
.then(|| Box::new(self.schema.read().clone()));
self.store_schema(schema)?;
if let Some(old) = old {
self.undo.record(UndoEntry::Schema(old));
}
Ok(())
}
pub(crate) fn commit_seq(&self) -> u64 {
self.undo.last_commit_seq
}
pub(crate) fn rollback_undo_unit(&mut self, mark: UnitMark, reason: &RollbackReason) {
let notify = self.undo.depth == 1 && self.undo.hooks.is_active() && self.undo.changed;
self.undo_to(mark);
self.undo.close();
if notify {
self.undo.hooks.after_rollback(reason);
}
}
pub(crate) fn undo_unit_changes(&self, mark: UnitMark) -> crate::graph::UnitChanges {
let mut changes = crate::graph::UnitChanges::NONE;
if self.undo.depth == 0 {
return changes;
}
for entry in self.undo.entries.iter().skip(mark_position(mark)) {
match entry {
UndoEntry::Schema(_) => changes.schema = true,
UndoEntry::Definition { .. } => changes.definitions = true,
UndoEntry::AddedVertex { .. }
| UndoEntry::AddedEdge { .. }
| UndoEntry::DroppedVertex { .. }
| UndoEntry::DroppedEdge(_)
| UndoEntry::VertexProperty { .. }
| UndoEntry::EdgeProperty { .. }
| UndoEntry::VertexLabels { .. }
| UndoEntry::EdgeLabel { .. } => changes.data = true,
}
if changes.data && changes.schema && changes.definitions {
break;
}
}
changes
}
pub(crate) fn undo_depth(&self) -> u32 {
self.undo.depth
}
pub(crate) fn discard_undo_unit(&mut self, mark: UnitMark) {
self.undo_to(mark);
self.undo.close();
}
pub(crate) fn discard_undo_unit_with_changes(&mut self, mark: UnitMark) -> ChangeSet {
let mutations = self.derive_mutations_since(mark_position(mark));
let stamp = CommitStamp {
commit_seq: 0,
committed_at: 0,
node_sequence: self.node_sequence,
edge_sequence: self.edge_sequence,
};
self.discard_undo_unit(mark);
ChangeSet::new(stamp, CommitMetadata::default(), mutations)
}
pub(crate) fn set_undo_metadata(&mut self, metadata: CommitMetadata) {
match self.undo.depth {
0 => {}
1 => {
let nested = std::mem::replace(&mut self.undo.metadata, metadata);
self.undo.metadata.merge_missing(nested);
}
_ => self.undo.metadata.merge_missing(metadata),
}
}
fn undo_to(&mut self, mark: UnitMark) {
let position = mark_position(mark);
while self.undo.entries.len() > position {
let Some(entry) = self.undo.entries.pop() else {
break;
};
self.undo_entry(entry);
}
}
fn undo_entry(&mut self, entry: UndoEntry) {
match entry {
UndoEntry::AddedVertex { id } => match self.index.get_node(&id) {
Some(handle) => {
self.remove_vertex_raw(handle, false);
}
None => tracing::error!(%id, "undo: added vertex not found"),
},
UndoEntry::AddedEdge { id } => match self.index.get_edge(&id) {
Some(handle) => {
self.remove_edge_raw(handle, false);
}
None => tracing::error!(%id, "undo: added edge not found"),
},
UndoEntry::DroppedVertex { vertex, edges } => {
let VertexImage {
id,
labels,
properties,
} = *vertex;
let handle = self.insert_vertex_raw(id, labels, properties);
self.compression_normalize_vertex(handle);
for edge in edges.into_iter().rev() {
self.restore_edge(edge);
}
}
UndoEntry::DroppedEdge(edge) => self.restore_edge(*edge),
UndoEntry::VertexProperty { id, key, old } => {
let vertex = self
.index
.get_node(&id)
.and_then(|handle| self.vertex_weight_mut(handle));
match vertex {
Some(vertex) => restore_property(&mut vertex.2, &key, old),
None => tracing::error!(%id, "undo: vertex of a property write not found"),
}
if let Some(handle) = self.index.get_node(&id) {
self.compression_normalize_vertex_key(handle, &key);
}
}
UndoEntry::EdgeProperty {
id,
key,
old,
map_was_none,
} => {
let edge = self
.index
.get_edge(&id)
.and_then(|handle| self.edge_weight_mut(handle));
match edge {
Some(edge) => {
if map_was_none {
edge.2 = None;
} else {
restore_property(
edge.2.get_or_insert_with(PropertyMap::default),
&key,
old,
);
}
}
None => tracing::error!(%id, "undo: edge of a property write not found"),
}
if let Some(handle) = self.index.get_edge(&id) {
self.compression_normalize_edge_key(handle, &key);
}
}
UndoEntry::VertexLabels { id, old } => {
let Some(handle) = self.index.get_node(&id) else {
tracing::error!(%id, "undo: vertex of a label change not found");
return;
};
let Some(vertex) = self.vertex_weight_mut(handle) else {
tracing::error!(%id, "undo: vertex of a label change not found");
return;
};
let current = std::mem::replace(&mut vertex.1, old.clone());
self.statistics.vertex_relabeled(¤t, &old);
for label in current.iter().filter(|l| !old.contains(l)) {
self.index.remove_node_label(label, handle);
}
for label in old.into_iter().filter(|l| !current.contains(l)) {
self.index.insert_node_label(label, handle);
}
self.compression_normalize_vertex(handle);
}
UndoEntry::EdgeLabel { id, old } => {
let Some(handle) = self.index.get_edge(&id) else {
tracing::error!(%id, "undo: edge of a label change not found");
return;
};
let Some(edge) = self.edge_weight_mut(handle) else {
tracing::error!(%id, "undo: edge of a label change not found");
return;
};
let current = std::mem::replace(&mut edge.1, old.clone());
self.statistics
.edge_relabeled(current.as_deref(), old.as_deref());
if let Some(current) = current {
self.index.remove_edge_label(¤t, handle);
}
if let Some(old) = old {
self.index.insert_edge_label(old, handle);
}
self.compression_normalize_edge(handle);
}
UndoEntry::Schema(old) => *self.schema.write() = *old,
UndoEntry::Definition { kind, name, old } => {
let replaced = match old {
Some(old) => self.definitions.insert(*old),
None => self.definitions.remove(kind, &name),
};
if kind == DefinitionKind::COMPRESSION {
self.compression_after_change(replaced.as_ref(), &name);
}
}
}
}
pub(crate) fn set_definition_recorded(
&mut self,
definition: Definition,
) -> Result<(), GraphError> {
definition.validate()?;
crate::graph::compression::check_conflict(self, &definition)?;
let current = self.definitions.get(definition.kind(), definition.name());
if current == Some(&definition) {
return Ok(());
}
if current.is_none() && self.definitions.len() >= crate::graph::definition::MAX_DEFINITIONS
{
return Err(crate::error::DefinitionError::TooManyDefinitions {
limit: crate::graph::definition::MAX_DEFINITIONS,
}
.into());
}
let kind = definition.kind();
let name = CompactString::from(definition.name());
let old = self.definitions.insert(definition);
if kind == DefinitionKind::COMPRESSION {
self.compression_after_change(old.as_ref(), &name);
}
self.undo.record(UndoEntry::Definition {
kind,
name,
old: old.map(Box::new),
});
Ok(())
}
pub(crate) fn remove_definition_recorded(&mut self, kind: DefinitionKind, name: &str) -> bool {
match self.definitions.remove(kind, name) {
Some(old) => {
if kind == DefinitionKind::COMPRESSION {
self.compression_after_change(Some(&old), name);
}
self.undo.record(UndoEntry::Definition {
kind,
name: CompactString::from(name),
old: Some(Box::new(old)),
});
true
}
None => false,
}
}
pub(crate) fn record_vertex_property(&mut self, node: VertexHandle, key: &str) {
if !self.undo.is_recording() {
return;
}
let Some(vertex) = self.vertex_weight(node) else {
return;
};
let entry = UndoEntry::VertexProperty {
id: vertex.0.clone(),
key: CompactString::from(key),
old: vertex
.2
.0
.get_full(key)
.map(|(position, _, value)| (position, value.clone())),
};
self.undo.record(entry);
}
pub(crate) fn record_edge_property(&mut self, edge: EdgeHandle, key: &str) {
if !self.undo.is_recording() {
return;
}
let Some(edge) = self.edge_weight(edge) else {
return;
};
let entry = UndoEntry::EdgeProperty {
id: edge.0.clone(),
key: CompactString::from(key),
old: edge
.2
.as_ref()
.and_then(|properties| properties.0.get_full(key))
.map(|(position, _, value)| (position, value.clone())),
map_was_none: edge.2.is_none(),
};
self.undo.record(entry);
}
fn restore_edge(&mut self, edge: EdgeImage) {
let EdgeImage {
id,
label,
properties,
source,
target,
} = edge;
match (self.index.get_node(&source), self.index.get_node(&target)) {
(Some(from), Some(to)) => {
let handle = self.insert_edge_raw(id, label, properties, from, to);
self.compression_normalize_edge(handle);
}
_ => tracing::error!(%id, "undo: endpoint of a dropped edge not found"),
}
}
pub(crate) fn insert_vertex_raw(
&mut self,
id: CompactString,
labels: Vec<CompactString>,
properties: PropertyMap,
) -> VertexHandle {
let index = self.graph.add_node((
id.clone(),
labels,
properties,
crate::graph::handle::FIRST_GENERATION,
));
let generation = self.node_generations.current(index.index());
let handle = VertexHandle::new(index, generation);
self.index.insert_node(id, handle);
if let Some(vertex) = self.graph.node_weight_mut(index) {
vertex.3 = generation;
self.statistics.vertex_added(&vertex.1);
for label in &vertex.1 {
self.index.insert_node_label(label.clone(), handle);
}
}
handle
}
pub(crate) fn insert_edge_raw(
&mut self,
id: CompactString,
label: Option<CompactString>,
properties: Option<PropertyMap>,
from: VertexHandle,
to: VertexHandle,
) -> EdgeHandle {
let mut generations = EdgeGenerations {
own: crate::graph::handle::FIRST_GENERATION,
source: from.generation(),
target: to.generation(),
};
let edge_index = self.graph.add_edge(
from.index(),
to.index(),
(id.clone(), label.clone(), properties, generations),
);
generations.own = self.edge_generations.current(edge_index.index());
if let Some(edge) = self.graph.edge_weight_mut(edge_index) {
edge.3 = generations;
}
let handle = EdgeHandle::new(edge_index, generations.own);
self.statistics.edge_added(label.as_deref());
self.index.insert_edge(id, handle);
if let Some(label) = label {
self.index.insert_edge_label(label, handle);
}
handle
}
pub(crate) fn remove_edge_raw(&mut self, edge: EdgeHandle, capture: bool) -> Option<EdgeImage> {
let index = self.resolve_edge(edge)?;
self.remove_edge_at(index, edge, capture)
}
fn remove_edge_at(
&mut self,
index: EdgeIndex,
edge: EdgeHandle,
capture: bool,
) -> Option<EdgeImage> {
let endpoints = if capture {
self.graph.edge_endpoints(index).and_then(|(from, to)| {
let source = self.graph.node_weight(from)?.0.clone();
let target = self.graph.node_weight(to)?.0.clone();
Some((source, target))
})
} else {
None
};
let (id, label, properties, _) = self.graph.remove_edge(index)?;
self.statistics.edge_removed(label.as_deref());
if let Some(label) = &label {
self.index.remove_edge_label(label, edge);
}
self.index.remove_edge(&id);
self.edge_generations.bump(index.index());
let (source, target) = endpoints?;
Some(EdgeImage {
id,
label,
properties,
source,
target,
})
}
pub(crate) fn remove_vertex_raw(
&mut self,
node: VertexHandle,
capture: bool,
) -> Option<UndoEntry> {
let index = self.resolve_vertex(node)?;
let mut incident: Vec<EdgeHandle> = Vec::new();
for direction in [Direction::Outgoing, Direction::Incoming] {
for edge in self.graph.edges_directed(index, direction) {
if direction == Direction::Incoming && edge.source() == index {
continue;
}
incident.push(EdgeHandle::new(edge.id(), edge.weight().3.own));
}
}
let mut edges = Vec::new();
if capture {
edges.reserve_exact(incident.len());
}
for edge in incident {
if let Some(image) = self.remove_edge_at(edge.index(), edge, capture) {
edges.push(image);
}
}
let (id, labels, properties, _) = self.graph.remove_node(index)?;
self.statistics.vertex_removed(&labels);
self.index.remove_node(&id);
for label in &labels {
self.index.remove_node_label(label, node);
}
self.node_generations.bump(index.index());
capture.then(|| UndoEntry::DroppedVertex {
vertex: Box::new(VertexImage {
id,
labels,
properties,
}),
edges,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::GraphStorage;
#[test]
fn nothing_is_recorded_outside_a_unit() {
let mut graph = TraversalGraph::tinkerpop_modern();
let v =
GraphStorage::add_vertex(&mut graph, None, vec!["x".into()], PropertyMap::default())
.expect("add");
GraphStorage::set_vertex_property(&mut graph, v, "k".into(), 1.into()).expect("set");
assert_eq!(graph.undo.len(), 0);
}
#[test]
fn outermost_commit_clears_and_nested_commit_keeps() {
let mut graph = TraversalGraph::new();
let outer = graph.begin_unit();
GraphStorage::add_vertex(&mut graph, None, vec![], PropertyMap::default()).expect("add");
let inner = graph.begin_unit();
GraphStorage::add_vertex(&mut graph, None, vec![], PropertyMap::default()).expect("add");
graph.commit_unit(inner).expect("commit");
assert_eq!(graph.undo.len(), 2);
graph.commit_unit(outer).expect("commit");
assert_eq!(graph.undo.len(), 0);
assert!(!graph.undo.is_recording());
assert_eq!(graph.vertex_count(), 2);
}
#[test]
fn restore_property_puts_the_key_back_at_its_position() {
let mut props = PropertyMap::default();
props.insert("a", 1.into());
props.insert("b", 2.into());
props.insert("c", 3.into());
let (position, _, value) = props.0.shift_remove_full("b").expect("b");
restore_property(&mut props, "b", Some((position, value)));
let keys: Vec<&str> = props.keys().map(|k| k.as_str()).collect();
assert_eq!(keys, ["a", "b", "c"]);
props.insert("d", 4.into());
restore_property(&mut props, "d", None);
assert_eq!(props.len(), 3);
}
}