blob: 00d18416d04aca2eb52336da6ab2025a3c276230 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
//! One writer FIFO shares FULL commits across independent, typed durable statements.
//! Domain SQL stays with its owner; no effect token or notification escapes before commit.
use crate::{StoreError, connection::BoxFuture};
use sqlx::{Connection, SqliteConnection, sqlite::SqliteTransactionManager};
use sqlx_core::transaction::TransactionManager;
use std::sync::{
Arc,
atomic::{AtomicU64, Ordering},
};
use tokio::sync::{oneshot, watch};
pub(crate) trait Statement: Send + Sized + 'static {
type Value: Send + 'static;
type Output: Send + 'static;
fn execute(
self,
connection: &mut SqliteConnection,
) -> BoxFuture<'_, Result<Prepared<Self::Value>, StoreError>>;
// Pure domain conversion; sealed effect tokens are constructed only here.
fn committed(value: Self::Value) -> Self::Output;
fn isolated_rejection(error: &StoreError) -> bool;
// The original Run domains remain usable after a confirmed full rollback.
// Pure observation requests retain their stricter owner-retirement policy.
fn reusable_after_rollback(&self) -> bool {
true
}
}
pub(crate) enum Notification {
None,
Metadata,
Sequence(u64),
}
pub(crate) struct Prepared<T> {
pub value: T,
pub changed: bool,
pub notification: Notification,
}
enum State<S: Statement> {
Waiting(S),
Entering(S),
Writing,
Prepared(Prepared<S::Value>),
Rejected { error: StoreError, isolated: bool },
}
pub(crate) struct Pending<S: Statement> {
reusable_after_rollback: bool,
state: State<S>,
commits: watch::Sender<u64>,
reply: oneshot::Sender<Result<S::Output, StoreError>>,
failures: Arc<AtomicU64>,
}
impl<S: Statement> Pending<S> {
pub(crate) fn new(
statement: S,
commits: watch::Sender<u64>,
reply: oneshot::Sender<Result<S::Output, StoreError>>,
failures: Arc<AtomicU64>,
) -> Self {
Self {
reusable_after_rollback: statement.reusable_after_rollback(),
state: State::Waiting(statement),
commits,
reply,
failures,
}
}
fn enter(&mut self) {
let State::Waiting(statement) = std::mem::replace(&mut self.state, State::Writing) else {
unreachable!("durable statement enters once")
};
self.state = State::Entering(statement);
}
async fn prepare(&mut self, connection: &mut SqliteConnection) -> Result<bool, PrepareFailure> {
let State::Entering(statement) = std::mem::replace(&mut self.state, State::Writing) else {
unreachable!("durable statement is prepared once")
};
let reusable = self.reusable_after_rollback;
let prepared = statement
.execute(connection)
.await
.map_err(|cause| PrepareFailure { cause, reusable })?;
let changed = prepared.changed;
self.state = State::Prepared(prepared);
Ok(changed)
}
fn reject(&mut self, error: StoreError, isolated: bool) {
self.state = State::Rejected { error, isolated };
}
fn finish(self, transaction: &Result<(), Aborted>) {
let result = match self.state {
State::Rejected { error, isolated }
if isolated
|| transaction.is_ok()
|| transaction.as_ref().is_err_and(|failure| !failure.unknown) =>
{
Err(error)
}
State::Prepared(prepared) if transaction.is_ok() => {
match prepared.notification {
Notification::None => {}
Notification::Metadata => self.commits.send_modify(|_| {}),
Notification::Sequence(sequence) => {
self.commits.send_replace(sequence);
}
}
Ok(S::committed(prepared.value))
}
state => Err(transaction
.as_ref()
.expect_err("unprepared durable receipt")
.error(!matches!(state, State::Waiting(_)))),
};
if result.is_err() {
self.failures.fetch_add(1, Ordering::AcqRel);
}
let _ = self.reply.send(result);
}
}
// This closed family is the admission contract. Arbitrary Run closures never join it.
pub(crate) enum Request {
Intent(Box<Pending<crate::applications::intents::WriteIntent>>),
Dispatch(Box<Pending<crate::applications::operations::WriteDispatch>>),
Admission(Box<Pending<crate::message_admissions::WriteAdmission>>),
Append(Box<Pending<crate::append::write::WriteAppend>>),
Settlement(Box<Pending<crate::applications::operations::WriteSettlement>>),
}
impl Request {
fn reusable_after_rollback(&self) -> bool {
match self {
Self::Intent(value) => value.reusable_after_rollback,
Self::Dispatch(value) => value.reusable_after_rollback,
Self::Admission(value) => value.reusable_after_rollback,
Self::Append(value) => value.reusable_after_rollback,
Self::Settlement(value) => value.reusable_after_rollback,
}
}
fn enter(&mut self) {
match self {
Self::Intent(value) => value.enter(),
Self::Dispatch(value) => value.enter(),
Self::Admission(value) => value.enter(),
Self::Append(value) => value.enter(),
Self::Settlement(value) => value.enter(),
}
}
async fn prepare(&mut self, connection: &mut SqliteConnection) -> Result<bool, PrepareFailure> {
match self {
Self::Intent(value) => value.prepare(connection).await,
Self::Dispatch(value) => value.prepare(connection).await,
Self::Admission(value) => value.prepare(connection).await,
Self::Append(value) => value.prepare(connection).await,
Self::Settlement(value) => value.prepare(connection).await,
}
}
fn isolated_rejection(&self, error: &StoreError) -> bool {
match self {
Self::Intent(_) => crate::applications::intents::WriteIntent::isolated_rejection(error),
Self::Dispatch(_) => {
crate::applications::operations::WriteDispatch::isolated_rejection(error)
}
Self::Admission(_) => {
crate::message_admissions::WriteAdmission::isolated_rejection(error)
}
Self::Append(_) => crate::append::write::WriteAppend::isolated_rejection(error),
Self::Settlement(_) => {
crate::applications::operations::WriteSettlement::isolated_rejection(error)
}
}
}
fn reject(&mut self, error: StoreError, isolated: bool) {
match self {
Self::Intent(value) => value.reject(error, isolated),
Self::Dispatch(value) => value.reject(error, isolated),
Self::Admission(value) => value.reject(error, isolated),
Self::Append(value) => value.reject(error, isolated),
Self::Settlement(value) => value.reject(error, isolated),
}
}
fn finish(self, result: &Result<(), Aborted>) {
match self {
Self::Intent(value) => value.finish(result),
Self::Dispatch(value) => value.finish(result),
Self::Admission(value) => value.finish(result),
Self::Append(value) => value.finish(result),
Self::Settlement(value) => value.finish(result),
}
}
}
impl From<Pending<crate::applications::intents::WriteIntent>> for Request {
fn from(value: Pending<crate::applications::intents::WriteIntent>) -> Self {
Self::Intent(Box::new(value))
}
}
impl From<Pending<crate::applications::operations::WriteDispatch>> for Request {
fn from(value: Pending<crate::applications::operations::WriteDispatch>) -> Self {
Self::Dispatch(Box::new(value))
}
}
impl From<Pending<crate::message_admissions::WriteAdmission>> for Request {
fn from(value: Pending<crate::message_admissions::WriteAdmission>) -> Self {
Self::Admission(Box::new(value))
}
}
impl From<Pending<crate::append::write::WriteAppend>> for Request {
fn from(value: Pending<crate::append::write::WriteAppend>) -> Self {
Self::Append(Box::new(value))
}
}
impl From<Pending<crate::applications::operations::WriteSettlement>> for Request {
fn from(value: Pending<crate::applications::operations::WriteSettlement>) -> Self {
Self::Settlement(Box::new(value))
}
}
#[derive(Debug)]
struct Failure {
cause: StoreError,
cleanup: Option<sqlx::Error>,
}
impl std::fmt::Display for Failure {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.cause.fmt(formatter)?;
if let Some(cleanup) = &self.cleanup {
write!(formatter, "; rollback unconfirmed: {cleanup}")?;
}
Ok(())
}
}
impl std::error::Error for Failure {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match &self.cause {
StoreError::Sqlx(error) | StoreError::CommitUnknown(error) => Some(error),
error => Some(error),
}
}
}
struct Aborted {
failure: Arc<Failure>,
unknown: bool,
reusable: bool,
}
// Only statement-body errors may retain the original Run reuse policy. Boundary,
// authority and cleanup failures always retire the connection.
struct PrepareFailure {
cause: StoreError,
reusable: bool,
}
impl From<StoreError> for PrepareFailure {
fn from(cause: StoreError) -> Self {
Self {
cause,
reusable: false,
}
}
}
impl From<sqlx::Error> for PrepareFailure {
fn from(cause: sqlx::Error) -> Self {
StoreError::from(cause).into()
}
}
/// Keep SQLite's error category and extended code when several receipts share
/// the same original failure. Callers still inspect `as_database_error()`.
#[derive(Debug)]
struct SharedDatabaseError(Arc<Failure>);
impl SharedDatabaseError {
fn database(&self) -> &dyn sqlx::error::DatabaseError {
let (StoreError::Sqlx(error) | StoreError::CommitUnknown(error)) = &self.0.cause else {
unreachable!("shared database failure")
};
error.as_database_error().expect("original database error")
}
}
impl std::fmt::Display for SharedDatabaseError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(self.database(), formatter)?;
if let Some(cleanup) = &self.0.cleanup {
write!(formatter, "; rollback unconfirmed: {cleanup}")?;
}
Ok(())
}
}
impl std::error::Error for SharedDatabaseError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(self.database().as_error())
}
}
impl sqlx::error::DatabaseError for SharedDatabaseError {
fn message(&self) -> &str {
self.database().message()
}
fn code(&self) -> Option<std::borrow::Cow<'_, str>> {
self.database().code()
}
fn kind(&self) -> sqlx::error::ErrorKind {
self.database().kind()
}
fn as_error(&self) -> &(dyn std::error::Error + Send + Sync + 'static) {
self
}
fn as_error_mut(&mut self) -> &mut (dyn std::error::Error + Send + Sync + 'static) {
self
}
fn into_error(self: Box<Self>) -> Box<dyn std::error::Error + Send + Sync + 'static> {
self
}
}
impl Aborted {
fn rejected(error: StoreError) -> Self {
Self {
failure: Arc::new(Failure {
cause: error,
cleanup: None,
}),
unknown: false,
reusable: false,
}
}
fn error(&self, started: bool) -> StoreError {
// Preserve the original causal chain while sharing one transaction failure.
let error = match &self.failure.cause {
StoreError::Sqlx(error) | StoreError::CommitUnknown(error)
if error.as_database_error().is_some() =>
{
sqlx::Error::Database(Box::new(SharedDatabaseError(self.failure.clone())))
}
_ => sqlx::Error::Io(std::io::Error::other(self.failure.clone())),
};
if self.unknown && started {
StoreError::CommitUnknown(error)
} else if let StoreError::Io(original) = &self.failure.cause {
StoreError::Io(std::io::Error::new(original.kind(), self.failure.clone()))
} else {
StoreError::Sqlx(error)
}
}
}
pub(crate) async fn commit(
connection: &mut SqliteConnection,
mut requests: Vec<Request>,
validate: &(impl Fn() -> Result<(), StoreError> + Sync),
) -> bool {
let result = transaction(connection, &mut requests, validate).await;
let reusable = result
.as_ref()
.map_or_else(|failure| failure.reusable, |()| true);
for request in requests {
request.finish(&result);
}
reusable
}
async fn transaction(
connection: &mut SqliteConnection,
requests: &mut [Request],
validate: &(impl Fn() -> Result<(), StoreError> + Sync),
) -> Result<(), Aborted> {
validate().map_err(Aborted::rejected)?;
let recover_commit = requests.iter().all(Request::reusable_after_rollback);
let mut tx = connection
.begin_with("BEGIN IMMEDIATE")
.await
.map_err(|error| Aborted::rejected(error.into()))?;
match prepare(&mut tx, requests, validate).await {
Ok(true) => match tx.commit().await {
Ok(()) => Ok(()),
Err(error) => {
// A failed COMMIT still produces no token. For the original Run
// domains, explicitly await SQLx's rollback acknowledgment after
// the consumed transaction's queued cleanup; Drop is no receipt.
let (cleanup, reusable) = if recover_commit {
match SqliteTransactionManager::rollback(connection).await {
Ok(()) => (None, !connection.is_in_transaction() && validate().is_ok()),
Err(cleanup) => (Some(cleanup), false),
}
} else {
(None, false)
};
Err(Aborted {
failure: Arc::new(Failure {
cause: StoreError::CommitUnknown(error),
cleanup,
}),
unknown: true,
reusable,
})
}
},
Ok(false) => tx.rollback().await.map_err(|error| Aborted {
failure: Arc::new(Failure {
cause: error.into(),
cleanup: None,
}),
unknown: true,
reusable: false,
}),
Err(error) => {
let rollback = tx.rollback().await;
Err(match rollback {
Ok(()) => {
let reusable = error.reusable && validate().is_ok();
let mut failure = Aborted::rejected(error.cause);
failure.reusable = reusable;
failure
}
Err(cleanup) => Aborted {
failure: Arc::new(Failure {
cause: error.cause,
cleanup: Some(cleanup),
}),
unknown: true,
reusable: false,
},
})
}
}
}
async fn prepare(
connection: &mut SqliteConnection,
requests: &mut [Request],
validate: &(impl Fn() -> Result<(), StoreError> + Sync),
) -> Result<bool, PrepareFailure> {
let mut changed = false;
let isolate = requests.len() > 1;
for request in requests {
validate()?;
request.enter();
if isolate {
sqlx::query("SAVEPOINT durable_request")
.execute(&mut *connection)
.await?;
}
match request.prepare(connection).await {
Ok(written) => {
if isolate {
sqlx::query("RELEASE durable_request")
.execute(&mut *connection)
.await?;
}
changed |= written;
}
Err(error) if request.isolated_rejection(&error.cause) => {
if isolate {
sqlx::query("ROLLBACK TO durable_request")
.execute(&mut *connection)
.await?;
sqlx::query("RELEASE durable_request")
.execute(&mut *connection)
.await?;
}
request.reject(error.cause, isolate);
}
Err(error) => return Err(error),
}
}
validate()?;
Ok(changed)
}
#[cfg(test)]
mod tests;