#[cfg(any(test, feature = "migration"))]
use crate::db::schema::preflight_schema_migration_record_op;
use crate::{
db::{
commit::{
PreparedRowCommitOp,
marker::{CommitMarker, DatabaseControlOp},
store::{with_commit_store, with_initialized_commit_store},
},
integrity::preflight_mutation_progress_record_op,
schema::preflight_schema_application_record_op,
},
error::InternalError,
traits::CanisterKind,
};
use std::panic::{AssertUnwindSafe, catch_unwind};
enum ApplyRollback {
None,
Closure(Box<dyn FnOnce()>),
SinglePreparedRow(PreparedRowCommitOp),
}
pub(crate) struct CommitApplyGuard {
finished: bool,
rollback: ApplyRollback,
}
impl CommitApplyGuard {
pub(crate) const fn new(_phase: &'static str) -> Self {
Self {
finished: false,
rollback: ApplyRollback::None,
}
}
pub(crate) fn record_rollback(&mut self, rollback: impl FnOnce() + 'static) {
debug_assert!(
matches!(self.rollback, ApplyRollback::None),
"commit apply guard currently owns exactly one rollback closure",
);
if matches!(self.rollback, ApplyRollback::None) {
self.rollback = ApplyRollback::Closure(Box::new(rollback));
}
}
pub(crate) fn record_single_row_rollback(&mut self, rollback: PreparedRowCommitOp) {
debug_assert!(
matches!(self.rollback, ApplyRollback::None),
"commit apply guard currently owns exactly one rollback payload",
);
if matches!(self.rollback, ApplyRollback::None) {
self.rollback = ApplyRollback::SinglePreparedRow(rollback);
}
}
pub(crate) fn finish(mut self) -> Result<(), InternalError> {
if self.finished {
return Err(InternalError::executor_invariant());
}
self.finished = true;
self.rollback = ApplyRollback::None;
Ok(())
}
fn rollback_best_effort(&mut self) {
if self.finished {
return;
}
match std::mem::replace(&mut self.rollback, ApplyRollback::None) {
ApplyRollback::None => {}
ApplyRollback::Closure(rollback) => {
let _ = catch_unwind(AssertUnwindSafe(rollback));
}
ApplyRollback::SinglePreparedRow(rollback) => {
let _ = catch_unwind(AssertUnwindSafe(|| rollback.apply()));
}
}
}
}
impl Drop for CommitApplyGuard {
fn drop(&mut self) {
if !self.finished {
self.rollback_best_effort();
}
}
}
#[derive(Clone, Debug)]
pub(crate) struct CommitGuard;
impl CommitGuard {
const fn new() -> Self {
Self
}
fn clear() -> Result<(), InternalError> {
with_initialized_commit_store(super::store::CommitStore::clear_verified)?
}
}
pub(crate) fn begin_commit(marker: CommitMarker) -> Result<CommitGuard, InternalError> {
begin_commit_with_preflighted_mutation_progress(marker, false)
}
pub(in crate::db) fn begin_mutation_progress_commit<C: CanisterKind>(
marker: CommitMarker,
) -> Result<CommitGuard, InternalError> {
let mut progress_count = 0_usize;
for operation in marker.database_control() {
if let DatabaseControlOp::MutationProgress(operation) = operation {
progress_count = progress_count.saturating_add(1);
preflight_mutation_progress_record_op::<C>(operation)?;
}
}
if progress_count != 1 {
return Err(InternalError::store_invariant());
}
begin_commit_with_preflighted_mutation_progress(marker, true)
}
fn begin_commit_with_preflighted_mutation_progress(
marker: CommitMarker,
mutation_progress_preflighted: bool,
) -> Result<CommitGuard, InternalError> {
for operation in marker.database_control() {
match operation {
DatabaseControlOp::SchemaApplication(operation) => {
preflight_schema_application_record_op(operation)?;
}
#[cfg(any(test, feature = "migration"))]
DatabaseControlOp::EntitySourceLineage(operation) => {
crate::db::schema::preflight_entity_source_lineage_catalog_op(operation)?;
}
#[cfg(any(test, feature = "migration"))]
DatabaseControlOp::SchemaMigration(operation) => {
preflight_schema_migration_record_op(operation)?;
}
DatabaseControlOp::MutationProgress(_) => {
if !mutation_progress_preflighted {
return Err(InternalError::store_invariant());
}
}
}
}
with_commit_store(|store| {
store.set_if_empty(&marker)?;
Ok(CommitGuard::new())
})
}
pub(crate) fn finish_commit(
mut guard: CommitGuard,
apply: impl FnOnce(&mut CommitGuard) -> Result<(), InternalError>,
) -> Result<(), InternalError> {
let result = apply(&mut guard);
if result.is_ok() {
CommitGuard::clear()?;
if !with_initialized_commit_store(super::store::CommitStore::is_empty)? {
return Err(InternalError::commit_corruption());
}
} else {
if with_initialized_commit_store(super::store::CommitStore::is_empty)? {
return Err(InternalError::commit_corruption());
}
}
result
}