use super::NativeGraphError;
use super::base::NativeAdmittedBase;
use super::write::{CommitStep, commit_staged_batch};
use crate::property_graph::query::completed::CompletedError;
use crate::property_graph::query::resources::QueryMemory;
use crate::property_graph::query::runtime::{
NativeExecutionError, RuntimeContext, RuntimeError, RuntimeLimits, WorkCounters,
};
use crate::property_graph::resources::GraphResources;
use crate::property_graph::staging::{
GraphBatchReadView, ItemReceipt, StageError, StatementImages, WriteControl, WriteLimits,
WriteMemory,
};
use crate::property_graph::storage::memory::StorageMemory;
use crate::property_graph::storage::tree::directory::{TreeError, TreeResources};
use crate::property_graph::storage::{
GraphReadView, NativeCatalog, NativePreparationSource, NativeQuerySource, NativeReadCapability,
};
use crate::property_graph::{BatchDisposition, GraphGeneration};
use std::cell::{Cell, RefCell};
use std::sync::Arc;
use std::sync::atomic::Ordering;
struct WriterRetainedView<'a> {
store: &'a crate::lifecycle::Store,
lease: &'a super::NativeReadLease,
}
impl crate::property_graph::query::runtime::RetainedView for WriterRetainedView<'_> {
fn query_view(&self) -> &crate::property_graph::query::QueryView {
self.lease.query_view()
}
fn check_active(&self) -> Result<(), crate::property_graph::query::QueryError> {
use crate::property_graph::query::QueryError;
match self.store.state().map_err(|_| QueryError::Control)? {
crate::lifecycle::StoreState::Open => self.lease.check_active(),
crate::lifecycle::StoreState::Closing | crate::lifecycle::StoreState::Closed => {
Err(QueryError::ReadCancelled)
}
}
}
}
pub(crate) trait NativeMutationConsumer<T, E = NativeExecutionError> {
fn consume<'lease, 'm, 'g, 'w, 'i>(
&mut self,
view: &'w GraphReadView<'w, 'lease, 'm, 'g>,
runtime: &mut RuntimeContext<'lease, 'm, 'g>,
overlay: GraphBatchReadView<'w, 'static>,
images: &'w StatementImages<'i>,
control: &mut WriteControl<'_>,
) -> Result<(T, GraphBatchReadView<'w, 'static>), E>;
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct NativeMutationReport {
pub(crate) disposition: BatchDisposition,
pub(crate) admitted: GraphGeneration,
pub(crate) changed: Option<GraphGeneration>,
pub(crate) counters: WorkCounters,
}
#[derive(Debug)]
pub(crate) enum NativeMutationError {
Graph(NativeGraphError),
Execution(NativeExecutionError),
Completed(CompletedError),
}
impl From<NativeGraphError> for NativeMutationError {
fn from(error: NativeGraphError) -> Self {
Self::Graph(error)
}
}
impl From<NativeExecutionError> for NativeMutationError {
fn from(error: NativeExecutionError) -> Self {
Self::Execution(error)
}
}
impl From<StageError> for NativeMutationError {
fn from(error: StageError) -> Self {
Self::Graph(NativeGraphError::Stage(error))
}
}
impl From<TreeError> for NativeMutationError {
fn from(error: TreeError) -> Self {
Self::Graph(NativeGraphError::Read(error))
}
}
impl From<crate::lifecycle::StoreError> for NativeMutationError {
fn from(error: crate::lifecycle::StoreError) -> Self {
Self::Graph(NativeGraphError::Store(error))
}
}
impl std::fmt::Display for NativeMutationError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Graph(error) => error.fmt(formatter),
Self::Execution(error) => error.fmt(formatter),
Self::Completed(error) => write!(formatter, "graph result: {error:?}"),
}
}
}
impl std::error::Error for NativeMutationError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Graph(error) => Some(error),
Self::Execution(error) => Some(error),
Self::Completed(_) => None,
}
}
}
impl crate::lifecycle::Store {
#[allow(
clippy::too_many_arguments,
reason = "one admission carries its control, limits and five capacities"
)]
#[allow(
clippy::result_large_err,
reason = "keep typed graph errors allocation-free on failure paths"
)]
pub(crate) fn with_native_mutation<T, C: NativeMutationConsumer<T>>(
&self,
control: &crate::lifecycle::QueryControl,
limits: RuntimeLimits,
memory_limit: usize,
source_slots: usize,
lazy_targets: usize,
overlay_capacity: usize,
image_capacity: usize,
consumer: C,
) -> Result<(T, NativeMutationReport), NativeMutationError> {
match self.native_mutation_attempts(
control,
limits,
memory_limit,
source_slots,
lazy_targets,
overlay_capacity,
image_capacity,
consumer,
|value, _, _| value,
) {
Ok(Ok(value)) => Ok(value),
Ok(Err(error)) => Err(error.into()),
Err(error) => Err(error),
}
}
#[allow(
clippy::too_many_arguments,
reason = "one admission carries its control, limits, five capacities and its settle step"
)]
pub(crate) fn with_native_mutation_settled<S, T, E, C, F>(
&self,
control: &crate::lifecycle::QueryControl,
limits: RuntimeLimits,
memory_limit: usize,
source_slots: usize,
lazy_targets: usize,
overlay_capacity: usize,
image_capacity: usize,
consumer: C,
settle: F,
) -> Result<(T, NativeMutationReport), E>
where
C: NativeMutationConsumer<S, E>,
E: From<NativeMutationError>,
F: FnOnce(S, &[ItemReceipt], Option<GraphGeneration>) -> T,
{
self.native_mutation_attempts(
control,
limits,
memory_limit,
source_slots,
lazy_targets,
overlay_capacity,
image_capacity,
consumer,
settle,
)
.unwrap_or_else(|error| Err(error.into()))
}
#[allow(
clippy::too_many_arguments,
clippy::type_complexity,
reason = "one admission carries its control, limits, five capacities and its settle step"
)]
#[allow(
clippy::result_large_err,
reason = "keep typed graph errors allocation-free on failure paths"
)]
fn native_mutation_attempts<S, T, E, C, F>(
&self,
control: &crate::lifecycle::QueryControl,
limits: RuntimeLimits,
memory_limit: usize,
source_slots: usize,
lazy_targets: usize,
overlay_capacity: usize,
image_capacity: usize,
mut consumer: C,
settle: F,
) -> Result<Result<(T, NativeMutationReport), E>, NativeMutationError>
where
C: NativeMutationConsumer<S, E>,
F: FnOnce(S, &[ItemReceipt], Option<GraphGeneration>) -> T,
{
self.native_graph.require_writable()?;
let mut maintenance_checked = false;
let mut run_maintenance = false;
let mut allow_pending_checkpoint = true;
loop {
if run_maintenance {
self.auto_maintain_native_graph(control)?;
run_maintenance = false;
}
let mut writer_slot = self.native_graph.writer.lock().map_err(|_| {
NativeGraphError::Store(crate::lifecycle::StoreError::Synchronization {
component: "native graph writer",
})
})?;
let writer = writer_slot
.as_mut()
.ok_or_else(|| self.absent_native_graph_writer())?;
if writer.stopped {
return Err(NativeGraphError::WritesStopped.into());
}
let lease = self.admit_native_read()?;
let admitted = Arc::clone(lease.bundle());
self.active_queries.fetch_add(1, Ordering::Relaxed);
let _active_query = crate::lifecycle::ActiveQuery {
count: &self.active_queries,
};
let shared = GraphResources::from_store(self)?;
let write_memory = WriteMemory::new(&shared, WriteLimits::default())?;
#[cfg(any(test, feature = "test-seams"))]
let (storage_limit, preparation_work) =
crate::property_graph::storage::search::native_vector_index_test_limits(
32 * 1024 * 1024,
64 * 1024 * 1024,
);
#[cfg(not(any(test, feature = "test-seams")))]
let (storage_limit, preparation_work) = (32 * 1024 * 1024, 64 * 1024 * 1024);
let storage = StorageMemory::new(&write_memory, control, storage_limit)?;
let preparation_checkpoint = || match self.state() {
Ok(crate::lifecycle::StoreState::Open) => Ok(()),
Ok(
crate::lifecycle::StoreState::Closing | crate::lifecycle::StoreState::Closed,
) => Err(TreeError::Control(
crate::lifecycle::QueryError::ReadCancelled { partial: false },
)),
Err(error) => Err(TreeError::Control(crate::lifecycle::QueryError::Store(
error,
))),
};
let preparation = NativePreparationSource::new(&lease, &storage, 64)?;
let mut base_resources = preparation
.resources(preparation_work)?
.with_preparation_checkpoint(&preparation_checkpoint)?;
let resources_cell = RefCell::new(&mut base_resources);
let first_storage_error = Cell::new(None);
let base = NativeAdmittedBase::with_lazy_targets(
&lease,
&preparation,
&storage,
&[],
&resources_cell,
&first_storage_error,
lazy_targets,
)?;
let memory = QueryMemory::new(&shared, memory_limit)
.map_err(RuntimeError::Memory)
.map_err(TreeError::Runtime)?;
let retained = WriterRetainedView {
store: self,
lease: &lease,
};
let mut runtime = RuntimeContext::new(&retained, control, &memory, limits)
.map_err(TreeError::Runtime)?;
let capability = NativeReadCapability::admit(&lease, &runtime)?;
let mut query_resources = TreeResources::for_query(&mut runtime)?;
let query_source = NativeQuerySource::new(capability, &query_resources, source_slots)?;
let catalog = NativeCatalog::open(&query_source, &mut query_resources)?;
drop(query_resources);
let view = GraphReadView::new(&query_source, &catalog)?;
let mut write_control = |phase| checkpoint(control, phase);
let images = StatementImages::new(
&write_memory,
admitted.document(),
image_capacity,
&mut write_control,
)?;
let overlay = GraphBatchReadView::new(
&base,
&write_memory,
overlay_capacity,
&mut write_control,
)?;
let consumed =
consumer.consume(&view, &mut runtime, overlay, &images, &mut write_control);
if let Some(error) = base.take_error() {
return Err(NativeGraphError::Stage(StageError::NativeStorage(error)).into());
}
let (value, overlay) = match consumed {
Ok(consumed) => consumed,
Err(error) => return Ok(Err(error)),
};
runtime.checkpoint().map_err(TreeError::Runtime)?;
let counters = runtime.counters();
let staged = overlay.finish(&mut write_control);
if let Some(error) = base.take_error() {
return Err(NativeGraphError::Stage(StageError::NativeStorage(error)).into());
}
let staged = staged?;
let disposition = staged.disposition();
let admitted_generation = admitted.base().generation;
if !maintenance_checked && disposition == BatchDisposition::Changed {
maintenance_checked = true;
if self.native_graph_maintenance_due()? {
run_maintenance = true;
continue;
}
}
let step = commit_staged_batch(
self,
writer,
&lease,
&admitted,
&shared,
&storage,
control,
&base,
&staged,
&mut allow_pending_checkpoint,
)?;
let changed = match step {
CommitStep::NoOp => None,
CommitStep::Committed { generation, .. } => Some(generation),
CommitStep::Checkpointed => continue,
};
return Ok(Ok((
settle(value, staged.receipts(), changed),
NativeMutationReport {
disposition,
admitted: admitted_generation,
changed,
counters,
},
)));
}
}
}
fn checkpoint(
control: &crate::lifecycle::QueryControl,
_: crate::property_graph::staging::WritePhase,
) -> Result<(), StageError> {
control.checkpoint().map_err(|_| StageError::Cancelled)
}