use std::net::SocketAddr;
use crate::bridge::envelope::{PhysicalPlan, Response, Status};
use crate::control::gateway::RouteDecision;
use crate::control::planner::calvin::{DispatchClass, classify_dispatch, read_vshards_of};
use crate::control::security::identity::AuthenticatedIdentity;
use crate::control::server::shared::plan_util::extract_collection;
use crate::control::state::SharedState;
use nodedb_cluster::calvin::types::ReleaseReason;
use nodedb_physical::physical_plan::MetaOp;
use nodedb_physical::physical_task::{PhysicalTask, PostSetOp};
use super::ddl_buffer;
use super::outcome::{AbortReason, CommitOutcome, TxnDataPlane};
use super::overlay_drop::drop_txn_overlay;
use super::read_set::ReadSetEntry;
use super::store::SessionStore;
pub async fn run_commit(
sessions: &SessionStore,
addr: &SocketAddr,
identity: &AuthenticatedIdentity,
state: &SharedState,
dp: &impl TxnDataPlane,
) -> CommitOutcome {
let read_set = sessions.take_read_set(addr);
let written_collections =
sessions.buffered_collections(addr, |plan| extract_collection(plan).map(String::from));
let buffered = sessions.buffered_tasks(addr);
let tenant_id = identity.tenant_id;
let read_vshards = read_vshards_of(&read_set);
if buffered.is_empty() {
if let Some(outcome) =
si_conflict_abort(sessions, addr, state, &read_set, &written_collections)
{
super::reservation_release::release_and_rollback(state, sessions, addr).await;
return outcome;
}
} else {
match classify_dispatch(&buffered, &read_vshards) {
DispatchClass::MultiShard { .. } => {
if let Some(reason) = super::commit_calvin::run_commit_calvin(
sessions, addr, state, &buffered, tenant_id, &read_set,
)
.await
{
super::reservation_release::release_and_rollback(state, sessions, addr).await;
return CommitOutcome::Aborted { reason };
}
}
DispatchClass::SingleShard { vshard: vshard_id } => {
let leader =
crate::control::server::graph_dispatch::cluster_resolve::resolve_for_vshard(
state,
vshard_id.as_u32(),
);
if !matches!(leader, RouteDecision::Local) {
if let Some(reason) = super::commit_calvin::run_commit_calvin(
sessions, addr, state, &buffered, tenant_id, &read_set,
)
.await
{
super::reservation_release::release_and_rollback(state, sessions, addr)
.await;
return CommitOutcome::Aborted { reason };
}
} else {
if let Some(outcome) =
si_conflict_abort(sessions, addr, state, &read_set, &written_collections)
{
super::reservation_release::release_and_rollback(state, sessions, addr)
.await;
return outcome;
}
if let Some(reason) =
dispatch_single_shard(state, dp, &buffered, tenant_id, vshard_id).await
{
super::reservation_release::release_and_rollback(state, sessions, addr)
.await;
return CommitOutcome::Aborted { reason };
}
}
}
}
}
super::reservation_release::release_session_reservations(
state,
sessions,
addr,
ReleaseReason::Commit,
)
.await;
match sessions.commit(addr) {
Ok(_) => {}
Err(_msg) => {
return CommitOutcome::Aborted {
reason: AbortReason::NoTransaction,
};
}
}
if let Some(txn_id) = buffered.first().and_then(|t| t.txn_id) {
let mut dropped = std::collections::HashSet::new();
for task in &buffered {
if dropped.insert(task.vshard_id) {
if let Err(e) = drop_txn_overlay(state, dp, tenant_id, task.vshard_id, txn_id).await
{
tracing::error!(
vshard = task.vshard_id.as_u32(),
error = %e,
"failed to release per-transaction staging overlay after commit"
);
}
}
}
}
let pending_offsets = sessions.take_pending_offsets(addr);
for (tid, stream, group, partition_id, lsn) in pending_offsets {
if let Err(e) = state
.offset_store
.commit_offset(tid, &stream, &group, partition_id, lsn)
{
tracing::warn!(
stream = %stream,
group = %group,
partition = partition_id,
error = %e,
"failed to commit deferred offset"
);
}
}
let reservations = sessions.take_pending_reservations(addr);
for handle in &reservations {
state.sequence_registry.gap_free_manager().commit(handle);
{
let catalog = state.credentials.catalog();
crate::control::sequence::log::log_reservation(
catalog,
&crate::control::sequence::log::committed(
&handle.sequence_key,
handle.value,
&identity.username,
identity.tenant_id.as_u64(),
),
);
}
}
if let Some(reason) = ddl_buffer::flush(state) {
return CommitOutcome::Aborted { reason };
}
sessions.close_non_hold_cursors(addr);
sessions.flush_pending_notifies(addr, identity.tenant_id, &state.notify_bus);
CommitOutcome::Committed
}
fn si_conflict_abort(
sessions: &SessionStore,
addr: &SocketAddr,
state: &SharedState,
read_set: &[ReadSetEntry],
written_collections: &std::collections::HashSet<String>,
) -> Option<CommitOutcome> {
let snapshot_lsn = sessions.snapshot_lsn(addr)?;
let current_lsn = state.wal.next_lsn();
let current = crate::types::Lsn::new(current_lsn.as_u64().saturating_sub(1));
for entry in read_set {
let collection = &entry.collection;
let read_lsn = entry.read_lsn;
if written_collections.contains(collection) {
continue;
}
if current > read_lsn && current > snapshot_lsn {
super::hot_key::record_read_set_aborts(state, read_set);
return Some(CommitOutcome::Aborted {
reason: AbortReason::Serialization,
});
}
}
None
}
async fn dispatch_single_shard(
state: &SharedState,
dp: &impl TxnDataPlane,
buffered: &[PhysicalTask],
tenant_id: crate::types::TenantId,
vshard_id: crate::types::VShardId,
) -> Option<AbortReason> {
let plans: Vec<PhysicalPlan> = buffered.iter().map(|t| t.plan.clone()).collect();
let database_id = buffered
.first()
.map_or(crate::types::DatabaseId::DEFAULT, |task| task.database_id);
if buffered.iter().any(|task| task.database_id != database_id) {
return Some(AbortReason::Dispatch(crate::Error::BadRequest {
detail: "transaction spans multiple databases".to_owned(),
}));
}
let Some(txn_id) = buffered.first().and_then(|t| t.txn_id) else {
return Some(AbortReason::Dispatch(crate::Error::Internal {
detail: "single-shard commit: buffered task carries no txn_id".into(),
}));
};
let resolve_task = PhysicalTask {
tenant_id,
vshard_id,
database_id,
plan: PhysicalPlan::Meta(MetaOp::ResolveTxn {
txn_id,
plans: plans.clone(),
}),
post_set_op: PostSetOp::None,
txn_id: None,
};
let resolve_resp = match dp.dispatch_no_wal(resolve_task, None).await {
Ok(r) if r.status == Status::Ok => r,
Ok(r) => {
return Some(AbortReason::BatchRejected {
code: r.error_code.as_deref().cloned(),
});
}
Err(e) => return Some(AbortReason::Dispatch(e)),
};
let redo = match crate::wal::RedoRecord::from_bytes(resolve_resp.payload.as_bytes()) {
Ok(r) => r,
Err(e) => {
return Some(AbortReason::Dispatch(crate::Error::Internal {
detail: format!("single-shard commit: resolve redo decode failed: {e}"),
}));
}
};
if !matches!(
crate::control::server::graph_dispatch::cluster_resolve::resolve_for_vshard(
state,
vshard_id.as_u32(),
),
RouteDecision::Local
) {
return Some(AbortReason::Serialization);
}
let wal_lsn = if redo.ops.is_empty() {
None
} else {
match state
.wal
.append_transaction_redo(tenant_id, vshard_id, database_id, &redo)
{
Ok(lsn) => Some(lsn),
Err(e) => {
return Some(AbortReason::Dispatch(crate::Error::Internal {
detail: format!("single-shard commit: transaction redo WAL append failed: {e}"),
}));
}
}
};
let batch_task = PhysicalTask {
tenant_id,
vshard_id,
database_id,
plan: PhysicalPlan::Meta(MetaOp::TransactionBatch {
plans,
txn_id: Some(txn_id),
}),
post_set_op: PostSetOp::None,
txn_id: None,
};
classify_batch_dispatch(dp.dispatch_no_wal(batch_task, wal_lsn).await)
}
pub(super) fn classify_batch_dispatch(result: crate::Result<Response>) -> Option<AbortReason> {
match result {
Err(e) => {
tracing::warn!(error = %e, "transaction batch dispatch failed");
Some(AbortReason::Dispatch(e))
}
Ok(resp) if resp.status != Status::Ok => Some(AbortReason::BatchRejected {
code: resp.error_code.as_deref().cloned(),
}),
Ok(_) => None,
}
}