use std::sync::Arc;
use fsqlite_error::{FrankenError, Result};
use fsqlite_types::cx::Cx;
use fsqlite_types::{CommitSeq, ObjectId, TxnToken};
use fsqlite_vfs::VfsFile;
use super::super::DurableCommitAcknowledgement;
use super::{
DurableWriteCoordinator, NativeCommitProof, NativeCoordinatorRecovery, NativeObjectCodec,
checkpoint, corrupt,
};
#[derive(Debug, Clone)]
pub struct NativeCommitAttempt {
epoch: Arc<()>,
acknowledged_tip: CommitSeq,
acknowledged_marker: Option<ObjectId>,
proof_object_id: ObjectId,
proof_digest: [u8; 32],
expected: DurableCommitAcknowledgement,
}
impl NativeCommitAttempt {
#[must_use]
pub const fn txn_token(&self) -> TxnToken {
self.expected.txn_token
}
#[must_use]
pub const fn commit_seq(&self) -> CommitSeq {
self.expected.commit_seq
}
#[must_use]
pub const fn capsule_object_id(&self) -> ObjectId {
self.expected.capsule_object_id
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum NativeCommitOutcome {
Committed(DurableCommitAcknowledgement),
NotCommitted,
}
impl<S: VfsFile, M: VfsFile, C: NativeObjectCodec> DurableWriteCoordinator<S, M, C> {
pub(crate) fn published_snapshot_metadata(&self) -> Result<(CommitSeq, Option<u64>)> {
self.ready()?;
let sequence = self.coordinator.commit_seq_tip;
if self.coordinator.allocated_seq_tip != sequence {
return Err(FrankenError::Busy);
}
if self.log.published_tip() != sequence {
return Err(corrupt("native snapshot coordinator/log tip mismatch"));
}
let time = (sequence != CommitSeq::ZERO).then_some(self.coordinator.last_commit_time_ns);
Ok((sequence, time))
}
pub fn commit_attempt(&self, token: TxnToken) -> Result<Option<NativeCommitAttempt>> {
if self.closing {
return Err(FrankenError::BusyRecovery);
}
let Some(pending) = self
.coordinator
.batch
.pending
.iter()
.find(|pending| pending.submission.txn_token == token)
else {
return Ok(None);
};
let proof_digest = if let Some(proof) = self.proofs.get(&pending.allocated_seq) {
*blake3::hash(&proof.bytes).as_bytes()
} else {
let proof = NativeCommitProof {
commit_seq: pending.allocated_seq,
commit_time_unix_ns: pending.allocated_time_ns,
submission: pending.submission.clone(),
};
*blake3::hash(&proof.to_bytes()?).as_bytes()
};
Ok(Some(NativeCommitAttempt {
epoch: Arc::clone(&self.attempt_epoch),
acknowledged_tip: self.coordinator.commit_seq_tip,
acknowledged_marker: self.coordinator.prev_marker_id,
proof_object_id: pending.proof_object_id,
proof_digest,
expected: DurableCommitAcknowledgement {
txn_token: pending.submission.txn_token,
capsule_object_id: pending.submission.capsule_object_id,
commit_seq: pending.allocated_seq,
commit_time_unix_ns: pending.allocated_time_ns,
},
}))
}
}
impl<S: VfsFile, M: VfsFile, C: NativeObjectCodec> NativeCoordinatorRecovery<S, M, C> {
pub async fn resolve_commit(
&self,
cx: &Cx,
attempt: &NativeCommitAttempt,
) -> Result<NativeCommitOutcome> {
if self.closing || self.prepared.is_none() {
return Err(FrankenError::BusyRecovery);
}
checkpoint(cx)?;
if !self
.attempt_epoch
.as_ref()
.is_some_and(|epoch| Arc::ptr_eq(epoch, &attempt.epoch))
{
return Err(FrankenError::Abort);
}
self.log.validate_recovered_lengths(cx)?;
if attempt.acknowledged_tip == CommitSeq::ZERO {
if attempt.acknowledged_marker.is_some() {
return Err(corrupt("invalid native attempt genesis boundary"));
}
} else {
let ordinal = usize::try_from(attempt.acknowledged_tip.get() - 1)
.map_err(|_| FrankenError::TooBig)?;
let marker = self
.report
.markers
.get(ordinal)
.ok_or_else(|| corrupt("native attempt lost acknowledged history"))?;
if marker.commit_seq != attempt.acknowledged_tip
|| Some(ObjectId::derive_from_canonical_bytes(
&marker.to_record_bytes(),
)) != attempt.acknowledged_marker
{
return Err(corrupt("native attempt changed acknowledged history"));
}
}
let ordinal =
usize::try_from(attempt.commit_seq().get() - 1).map_err(|_| FrankenError::TooBig)?;
let Some(marker) = self.report.markers.get(ordinal) else {
if self.report.append_blocked() {
return Err(FrankenError::BusyRecovery);
}
checkpoint(cx)?;
return Ok(NativeCommitOutcome::NotCommitted);
};
if marker.commit_seq != attempt.commit_seq()
|| marker.commit_time_unix_ns != attempt.expected.commit_time_unix_ns
|| marker.capsule_object_id != attempt.capsule_object_id()
|| marker.proof_object_id != attempt.proof_object_id
{
return Err(FrankenError::Abort);
}
let records = self.log.read_object(cx, marker.proof_object_id).await?;
let bytes = self.codec.decode(cx, marker.proof_object_id, &records)?;
if blake3::hash(&bytes).as_bytes() != &attempt.proof_digest {
return Err(corrupt("native attempt proof changed after admission"));
}
let proof = NativeCommitProof::from_bytes(&bytes)?;
let acknowledgement = DurableCommitAcknowledgement {
txn_token: proof.submission.txn_token,
capsule_object_id: proof.submission.capsule_object_id,
commit_seq: proof.commit_seq,
commit_time_unix_ns: proof.commit_time_unix_ns,
};
if acknowledgement != attempt.expected {
return Err(corrupt("native attempt receipt does not match its proof"));
}
checkpoint(cx)?;
self.log.validate_recovered_lengths(cx)?;
Ok(NativeCommitOutcome::Committed(acknowledgement))
}
}
#[cfg(test)]
mod tests;