use std::time::Duration;
use khive_storage::error::{SqliteWriteFailure, SqliteWriteStage};
use khive_storage::{StorageCapability, StorageError, WriterTaskRequestState};
use thiserror::Error;
pub(crate) fn native_write_failure(
error: &(dyn std::error::Error + 'static),
stage: SqliteWriteStage,
) -> Option<SqliteWriteFailure> {
if let Some(error) = error.downcast_ref::<StorageError>() {
return match error {
StorageError::SqliteWrite { failure, .. } => Some(*failure),
StorageError::Driver { source, .. } => native_write_failure(source.as_ref(), stage),
StorageError::WriterTaskRequestFailed { source, .. } => {
native_write_failure(source.as_ref(), stage)
}
_ => None,
};
}
if let Some(error) = error.downcast_ref::<SqliteError>() {
return match error {
SqliteError::Write { stage, source } => native_write_failure(source, *stage),
SqliteError::WriteSettlementUnknown { failure } => Some(*failure),
SqliteError::Rusqlite(source) => native_write_failure(source, stage),
_ => None,
};
}
if let Some(
rusqlite::Error::SqliteFailure(code, _)
| rusqlite::Error::SqlInputError { error: code, .. },
) = error.downcast_ref::<rusqlite::Error>()
{
return Some(SqliteWriteFailure {
stage,
primary_code: code.extended_code & 0xff,
extended_code: code.extended_code,
settlement_unknown: false,
});
}
None
}
pub(crate) fn with_write_stage(error: StorageError, stage: SqliteWriteStage) -> StorageError {
match native_write_failure(&error, stage) {
Some(failure) => error.with_sqlite_write_failure(failure),
None => error,
}
}
pub(crate) fn statement_failure(error: StorageError) -> StorageError {
with_write_stage(error, SqliteWriteStage::Statement)
}
pub(crate) fn unknown_write_settlement(mut error: StorageError) -> StorageError {
if let StorageError::SqliteWrite { failure, .. } = &mut error {
failure.settlement_unknown = true;
}
error
}
pub const SQLITE_WAL_CAPACITY_REFUSED_STAGE: &str = "sqlite_wal_capacity_refused";
pub const SQLITE_WAL_CAPACITY_UNAVAILABLE_STAGE: &str = "sqlite_wal_capacity_unavailable";
#[derive(Debug, Error)]
pub enum SqliteError {
#[error("sqlite error: {source}")]
Write {
stage: SqliteWriteStage,
#[source]
source: rusqlite::Error,
},
#[error(transparent)]
RequestReadStopped(khive_storage::StorageError),
#[error("sqlite error: {0}")]
Rusqlite(#[from] rusqlite::Error),
#[error("invalid data: {0}")]
InvalidData(String),
#[error("pooled writer contains an inherited transaction; prior side effects are unknown")]
InheritedWriterTransaction,
#[error("writer transaction settlement is unknown; connection retired")]
WriterSettlementUnknown,
#[error("writer transaction settlement is unknown; connection retired")]
WriteSettlementUnknown { failure: SqliteWriteFailure },
#[error("writer refused: an earlier write's settlement is unknown; this write did not start")]
WriterPoisoned,
#[error("invalid data: timed out after {timeout:?} waiting for sqlite writer connection")]
WriterPoolCheckoutTimeout {
timeout: Duration,
},
#[error(
"refusing sqlite write on {volume}: {available_bytes} bytes available, \
at or below the {floor_bytes}-byte free-space floor plus \
{required_headroom_bytes} bytes of operation headroom"
)]
CapacityFloor {
volume: String,
available_bytes: u64,
floor_bytes: u64,
required_headroom_bytes: u64,
},
#[error("sqlite capacity admission unavailable in {phase} phase: {message}")]
CapacityUnavailable {
phase: khive_storage::CapacityUnavailablePhase,
message: String,
},
#[error(
"volume lease re-entry: this thread already holds the lease for this volume \
(held at {holder_site}, requested again at {requester_site})"
)]
VolumeLeaseReentry {
holder_site: String,
requester_site: String,
},
#[error("invalid WAL ceiling {bytes} bytes: exceeds supported SQLite file offsets")]
WalCeilingOffsetOverflow { bytes: u64 },
#[error("invalid WAL ceiling {bytes} bytes: {backend_kind} does not support WAL enforcement")]
WalCeilingUnsupported {
bytes: u64,
backend_kind: &'static str,
},
#[error(
"invalid WAL ceiling {bytes} bytes: page size {page_size} requires at least {minimum_bytes} bytes for one WAL frame"
)]
WalCeilingBelowMinimum {
bytes: u64,
page_size: u64,
minimum_bytes: u64,
},
#[error(
"{stage}: WAL ceiling {bytes} bytes cannot be enforced: missing {capability}",
stage = SQLITE_WAL_CAPACITY_UNAVAILABLE_STAGE
)]
WalCapacityUnavailable {
bytes: u64,
capability: &'static str,
},
#[error("invalid config: {0}")]
InvalidConfig(String),
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("migration v{version} failed: {error}")]
Migration {
version: u32,
error: String,
},
}
impl SqliteError {
pub fn write(error: rusqlite::Error, stage: SqliteWriteStage) -> Self {
if matches!(
error,
rusqlite::Error::SqliteFailure(..) | rusqlite::Error::SqlInputError { .. }
) {
Self::Write {
stage,
source: error,
}
} else {
Self::Rusqlite(error)
}
}
pub(crate) fn settlement_with_cause(
error: &(dyn std::error::Error + 'static),
stage: SqliteWriteStage,
) -> Self {
match native_write_failure(error, stage) {
Some(mut failure) => {
failure.settlement_unknown = true;
Self::WriteSettlementUnknown { failure }
}
None => Self::WriterSettlementUnknown,
}
}
pub fn write_failure(&self) -> Option<SqliteWriteFailure> {
match self {
Self::Write { stage, source } => native_write_failure(source, *stage),
Self::WriteSettlementUnknown { failure } => Some(*failure),
_ => None,
}
}
pub fn wal_capacity_stage(&self) -> Option<&'static str> {
match self {
Self::WalCapacityUnavailable { .. } => Some(SQLITE_WAL_CAPACITY_UNAVAILABLE_STAGE),
_ => None,
}
}
pub fn into_storage_error(
self,
capability: StorageCapability,
operation: &'static str,
) -> StorageError {
match self {
error @ Self::Write { stage, .. } => {
with_write_stage(StorageError::driver(capability, operation, error), stage)
}
Self::CapacityFloor {
volume,
available_bytes,
floor_bytes,
required_headroom_bytes,
} => StorageError::CapacityFloor {
capability: StorageCapability::Sql,
volume,
available_bytes,
floor_bytes,
required_headroom_bytes,
},
Self::CapacityUnavailable { phase, message } => StorageError::CapacityUnavailable {
capability: StorageCapability::Sql,
phase,
message,
},
Self::WriteSettlementUnknown { failure } => {
StorageError::writer_task_terminated(WriterTaskRequestState::SideEffectsUnknown)
.with_sqlite_write_failure(failure)
}
Self::InheritedWriterTransaction | Self::WriterSettlementUnknown => {
StorageError::writer_task_terminated(WriterTaskRequestState::SideEffectsUnknown)
}
Self::WriterPoisoned => {
StorageError::writer_task_terminated(WriterTaskRequestState::NotStarted)
}
other => StorageError::driver(capability, operation, other),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use khive_storage::CapacityUnavailablePhase;
#[test]
fn capacity_and_settlement_errors_keep_their_meaning_from_any_store() {
for capability in [StorageCapability::Entities, StorageCapability::Sql] {
let refused = SqliteError::CapacityFloor {
volume: "/volume".to_string(),
available_bytes: 99,
floor_bytes: 100,
required_headroom_bytes: 12,
}
.into_storage_error(capability, "write");
assert!(
matches!(
refused,
StorageError::CapacityFloor {
capability: StorageCapability::Sql,
available_bytes: 99,
floor_bytes: 100,
required_headroom_bytes: 12,
..
}
),
"{capability:?}: {refused:?}"
);
let unavailable = SqliteError::CapacityUnavailable {
phase: CapacityUnavailablePhase::LockContended,
message: "lease timed out".to_string(),
}
.into_storage_error(capability, "write");
assert!(
matches!(
unavailable,
StorageError::CapacityUnavailable {
capability: StorageCapability::Sql,
phase: CapacityUnavailablePhase::LockContended,
..
}
),
"{capability:?}: {unavailable:?}"
);
for unsettled in [
SqliteError::InheritedWriterTransaction,
SqliteError::WriterSettlementUnknown,
] {
let mapped = unsettled.into_storage_error(capability, "write");
assert!(
matches!(
mapped,
StorageError::WriterTaskTerminated {
request_state: WriterTaskRequestState::SideEffectsUnknown,
..
}
),
"{capability:?}: {mapped:?}"
);
}
let refused = SqliteError::WriterPoisoned.into_storage_error(capability, "write");
assert!(
matches!(
refused,
StorageError::WriterTaskTerminated {
request_state: WriterTaskRequestState::NotStarted,
..
}
),
"{capability:?}: {refused:?}"
);
}
}
}