use crate::Error;
use crate::control::cluster::calvin::executor::ollp::error::OllpError;
use crate::control::planner::calvin::preexec::{PreexecScan, run_preexec_scan};
use crate::control::planner::calvin::tx_class::collection_name_from_plan;
use crate::control::planner::calvin::{
DependentRetryArgs, build_dependent_tx_class, build_single_vshard_dependent_tx_class,
is_dependent_predicate, predicate_class_for_filters, run_dependent_with_retry,
submit_calvin_routed_assign,
};
use crate::control::planner::implicit_edges::{
EdgeFieldOverrides, EdgeUpdateCtx, append_implicit_edge_delete_tasks,
append_implicit_edge_update_tasks, parse_edge_field_overrides,
};
use crate::control::state::{CalvinApplyResult, SharedState};
use crate::types::{DatabaseId, TenantId, TraceId};
use nodedb_cluster::calvin::sequencer::error::SequencerError;
use nodedb_physical::physical_plan::{DocumentOp, OllpPredictedEdge, PhysicalPlan};
use nodedb_physical::physical_task::PhysicalTask;
enum EdgeLifecycle {
Delete,
Update(EdgeFieldOverrides),
}
pub struct DependentReconOutcome {
pub tasks_dispatched: u64,
pub apply_result: Option<crate::bridge::envelope::Response>,
}
fn extract_bulk_predicate_info(plan: &PhysicalPlan) -> (String, Vec<u8>) {
match plan {
PhysicalPlan::Document(DocumentOp::BulkUpdate {
collection,
filters,
..
})
| PhysicalPlan::Document(DocumentOp::BulkDelete {
collection,
filters,
..
}) => (collection.clone(), filters.clone()),
PhysicalPlan::Document(_)
| PhysicalPlan::Vector(_)
| PhysicalPlan::Graph(_)
| PhysicalPlan::Kv(_)
| PhysicalPlan::Text(_)
| PhysicalPlan::Columnar(_)
| PhysicalPlan::Timeseries(_)
| PhysicalPlan::Spatial(_)
| PhysicalPlan::Crdt(_)
| PhysicalPlan::Query(_)
| PhysicalPlan::Meta(_)
| PhysicalPlan::Array(_)
| PhysicalPlan::ClusterArray(_) => (String::new(), vec![]),
}
}
fn inject_ollp_surrogates(plan: &mut PhysicalPlan, surrogates: Vec<u32>) {
match plan {
PhysicalPlan::Document(DocumentOp::BulkUpdate {
ollp_predicted_surrogates,
..
})
| PhysicalPlan::Document(DocumentOp::BulkDelete {
ollp_predicted_surrogates,
..
}) => {
*ollp_predicted_surrogates = Some(surrogates);
}
PhysicalPlan::Document(_)
| PhysicalPlan::Vector(_)
| PhysicalPlan::Graph(_)
| PhysicalPlan::Kv(_)
| PhysicalPlan::Text(_)
| PhysicalPlan::Columnar(_)
| PhysicalPlan::Timeseries(_)
| PhysicalPlan::Spatial(_)
| PhysicalPlan::Crdt(_)
| PhysicalPlan::Query(_)
| PhysicalPlan::Meta(_)
| PhysicalPlan::Array(_)
| PhysicalPlan::ClusterArray(_) => {}
}
}
fn inject_ollp_predicted_edges(plan: &mut PhysicalPlan, mut edges: Vec<OllpPredictedEdge>) {
edges.sort_unstable();
match plan {
PhysicalPlan::Document(DocumentOp::BulkUpdate {
ollp_predicted_edges,
..
})
| PhysicalPlan::Document(DocumentOp::BulkDelete {
ollp_predicted_edges,
..
}) => {
*ollp_predicted_edges = Some(edges);
}
PhysicalPlan::Document(_)
| PhysicalPlan::Vector(_)
| PhysicalPlan::Graph(_)
| PhysicalPlan::Kv(_)
| PhysicalPlan::Text(_)
| PhysicalPlan::Columnar(_)
| PhysicalPlan::Timeseries(_)
| PhysicalPlan::Spatial(_)
| PhysicalPlan::Crdt(_)
| PhysicalPlan::Query(_)
| PhysicalPlan::Meta(_)
| PhysicalPlan::Array(_)
| PhysicalPlan::ClusterArray(_) => {}
}
}
pub fn plan_needs_implicit_edge_recon(
state: &SharedState,
tasks: &[PhysicalTask],
tenant_id: TenantId,
) -> crate::Result<Option<(String, DatabaseId)>> {
let Some(dep_task) = tasks.iter().find(|t| is_dependent_predicate(&t.plan)) else {
return Ok(None);
};
let coll = collection_name_from_plan(&dep_task.plan);
let db = dep_task.database_id;
let edge_bearing = {
let catalog = state.credentials.catalog();
catalog
.get_collection(db, tenant_id.as_u64(), &coll)?
.map(|c| c.has_implicit_edges)
.unwrap_or(false)
};
if edge_bearing {
Ok(Some((coll, db)))
} else {
Ok(None)
}
}
pub async fn dispatch_dependent_edge_recon(
state: &SharedState,
tasks: Vec<PhysicalTask>,
tenant_id: TenantId,
database_id: DatabaseId,
allow_single_vshard: bool,
) -> crate::Result<DependentReconOutcome> {
let orchestrator = state.ollp_orchestrator.get();
let registry = state
.calvin_completion_registry
.get()
.ok_or(Error::SequencerUnavailable)?;
let dep_task = tasks
.iter()
.find(|t| is_dependent_predicate(&t.plan))
.ok_or_else(|| Error::Internal {
detail: "dependent-edge recon dispatch invoked without a dependent-predicate task"
.to_owned(),
})?;
let orc = orchestrator.ok_or(Error::SequencerUnavailable)?;
let (dep_collection, dep_filter_bytes) = extract_bulk_predicate_info(&dep_task.plan);
let pred_class = predicate_class_for_filters(&dep_filter_bytes, &dep_collection);
let edge_mode = match &dep_task.plan {
PhysicalPlan::Document(DocumentOp::BulkDelete { .. }) => EdgeLifecycle::Delete,
PhysicalPlan::Document(DocumentOp::BulkUpdate { updates, .. }) => {
let overrides = parse_edge_field_overrides(updates)?;
EdgeLifecycle::Update(overrides)
}
PhysicalPlan::Document(_)
| PhysicalPlan::Vector(_)
| PhysicalPlan::Graph(_)
| PhysicalPlan::Kv(_)
| PhysicalPlan::Text(_)
| PhysicalPlan::Columnar(_)
| PhysicalPlan::Timeseries(_)
| PhysicalPlan::Spatial(_)
| PhysicalPlan::Crdt(_)
| PhysicalPlan::Query(_)
| PhysicalPlan::Meta(_)
| PhysicalPlan::Array(_)
| PhysicalPlan::ClusterArray(_) => {
return Err(Error::Internal {
detail: "dependent Calvin task is neither BulkUpdate nor BulkDelete".to_owned(),
});
}
};
let initial_predicted = run_preexec_scan(
state,
tenant_id,
database_id,
&dep_collection,
dep_filter_bytes.clone(),
)
.await?;
let timeout = std::time::Duration::from_secs(state.tuning.network.default_deadline_secs);
let ollp_max_retries = orc.ollp_max_retries() as u32;
let submit = |predicted: &PreexecScan| {
let surrogates = predicted.surrogates.clone();
let edges = predicted.edges.clone();
let tasks = &tasks;
let dep_collection = &dep_collection;
let edge_mode = &edge_mode;
async move {
let predicted_edges: Vec<OllpPredictedEdge> = edges
.iter()
.map(|e| OllpPredictedEdge {
surrogate: e.surrogate,
from: e.from.clone(),
to: e.to.clone(),
label: e.label.clone(),
})
.collect();
let mut edge_tasks: Vec<PhysicalTask> = Vec::new();
match edge_mode {
EdgeLifecycle::Delete => {
append_implicit_edge_delete_tasks(
state,
&mut edge_tasks,
tenant_id,
database_id,
TraceId::ZERO,
dep_collection,
&edges,
)
.await
.map_err(|_| OllpError::Sequencer(SequencerError::Unavailable))?;
}
EdgeLifecycle::Update(overrides) => {
append_implicit_edge_update_tasks(
EdgeUpdateCtx {
state,
tenant_id,
database_id,
trace_id: TraceId::ZERO,
collection: dep_collection,
},
&mut edge_tasks,
&edges,
&surrogates,
overrides,
)
.await
.map_err(|_| OllpError::Sequencer(SequencerError::Unavailable))?;
}
}
orc.submit_with_retry_via(
pred_class,
tenant_id,
|| {
let mut modified_tasks: Vec<PhysicalTask> = tasks
.iter()
.map(|t| {
let mut t = t.clone();
inject_ollp_surrogates(&mut t.plan, surrogates.clone());
inject_ollp_predicted_edges(&mut t.plan, predicted_edges.clone());
t
})
.collect();
modified_tasks.extend(edge_tasks.iter().cloned());
let built = if allow_single_vshard {
build_single_vshard_dependent_tx_class(
&modified_tasks,
tenant_id,
dep_collection,
&surrogates,
&[],
)
} else {
build_dependent_tx_class(
&modified_tasks,
tenant_id,
dep_collection,
&surrogates,
&[],
)
};
built.map_err(|_| {
nodedb_cluster::error::CalvinError::Sequencer(SequencerError::Unavailable)
})
},
|tx_class| async move {
submit_calvin_routed_assign(state, tx_class)
.await
.map_err(|_| OllpError::Sequencer(SequencerError::Unavailable))
},
)
.await
}
};
let rescan = || {
run_preexec_scan(
state,
tenant_id,
database_id,
&dep_collection,
dep_filter_bytes.clone(),
)
};
let completed_txn = run_dependent_with_retry(DependentRetryArgs {
registry,
orchestrator: orc,
predicate_class_hash: pred_class,
timeout,
ollp_max_retries,
initial_predicted,
submit,
rescan,
})
.await?;
let drained = state
.calvin_apply_results
.lock()
.unwrap_or_else(|p| p.into_inner())
.remove(&completed_txn);
let apply_result = match drained {
Some(CalvinApplyResult::Single { response, .. }) => Some(response),
Some(CalvinApplyResult::Conflict) => {
return Err(Error::Internal {
detail: "multi-participant cross-shard RETURNING not supported".to_owned(),
});
}
None => None,
};
Ok(DependentReconOutcome {
tasks_dispatched: tasks.len() as u64,
apply_result,
})
}