1use std::time::Duration;
4
5use khive_storage::error::{SqliteWriteFailure, SqliteWriteStage};
6use khive_storage::{StorageCapability, StorageError, WriterTaskRequestState};
7use thiserror::Error;
8
9pub(crate) fn native_write_failure(
12 error: &(dyn std::error::Error + 'static),
13 stage: SqliteWriteStage,
14) -> Option<SqliteWriteFailure> {
15 if let Some(error) = error.downcast_ref::<StorageError>() {
16 return match error {
17 StorageError::SqliteWrite { failure, .. } => Some(*failure),
18 StorageError::Driver { source, .. } => native_write_failure(source.as_ref(), stage),
19 StorageError::WriterTaskRequestFailed { source, .. } => {
20 native_write_failure(source.as_ref(), stage)
21 }
22 _ => None,
23 };
24 }
25 if let Some(error) = error.downcast_ref::<SqliteError>() {
26 return match error {
27 SqliteError::Write { stage, source } => native_write_failure(source, *stage),
28 SqliteError::WriteSettlementUnknown { failure } => Some(*failure),
29 SqliteError::Rusqlite(source) => native_write_failure(source, stage),
30 _ => None,
31 };
32 }
33 if let Some(
34 rusqlite::Error::SqliteFailure(code, _)
35 | rusqlite::Error::SqlInputError { error: code, .. },
36 ) = error.downcast_ref::<rusqlite::Error>()
37 {
38 return Some(SqliteWriteFailure {
39 stage,
40 primary_code: code.extended_code & 0xff,
41 extended_code: code.extended_code,
42 settlement_unknown: false,
43 });
44 }
45 None
46}
47
48pub(crate) fn with_write_stage(error: StorageError, stage: SqliteWriteStage) -> StorageError {
49 match native_write_failure(&error, stage) {
50 Some(failure) => error.with_sqlite_write_failure(failure),
51 None => error,
52 }
53}
54
55pub(crate) fn statement_failure(error: StorageError) -> StorageError {
56 with_write_stage(error, SqliteWriteStage::Statement)
57}
58
59pub(crate) fn unknown_write_settlement(mut error: StorageError) -> StorageError {
60 if let StorageError::SqliteWrite { failure, .. } = &mut error {
61 failure.settlement_unknown = true;
62 }
63 error
64}
65
66pub const SQLITE_WAL_CAPACITY_REFUSED_STAGE: &str = "sqlite_wal_capacity_refused";
69pub const SQLITE_WAL_CAPACITY_UNAVAILABLE_STAGE: &str = "sqlite_wal_capacity_unavailable";
70
71#[derive(Debug, Error)]
73pub enum SqliteError {
74 #[error("sqlite error: {source}")]
75 Write {
76 stage: SqliteWriteStage,
77 #[source]
78 source: rusqlite::Error,
79 },
80 #[error(transparent)]
82 RequestReadStopped(khive_storage::StorageError),
83
84 #[error("sqlite error: {0}")]
86 Rusqlite(#[from] rusqlite::Error),
87
88 #[error("invalid data: {0}")]
90 InvalidData(String),
91
92 #[error("pooled writer contains an inherited transaction; prior side effects are unknown")]
95 InheritedWriterTransaction,
96
97 #[error("writer transaction settlement is unknown; connection retired")]
99 WriterSettlementUnknown,
100
101 #[error("writer transaction settlement is unknown; connection retired")]
103 WriteSettlementUnknown { failure: SqliteWriteFailure },
104
105 #[error("writer refused: an earlier write's settlement is unknown; this write did not start")]
109 WriterPoisoned,
110
111 #[error("invalid data: timed out after {timeout:?} waiting for sqlite writer connection")]
120 WriterPoolCheckoutTimeout {
121 timeout: Duration,
123 },
124
125 #[error(
128 "refusing sqlite write on {volume}: {available_bytes} bytes available, \
129 at or below the {floor_bytes}-byte free-space floor plus \
130 {required_headroom_bytes} bytes of operation headroom"
131 )]
132 CapacityFloor {
133 volume: String,
134 available_bytes: u64,
135 floor_bytes: u64,
136 required_headroom_bytes: u64,
137 },
138
139 #[error("sqlite capacity admission unavailable in {phase} phase: {message}")]
142 CapacityUnavailable {
143 phase: khive_storage::CapacityUnavailablePhase,
144 message: String,
145 },
146
147 #[error(
151 "volume lease re-entry: this thread already holds the lease for this volume \
152 (held at {holder_site}, requested again at {requester_site})"
153 )]
154 VolumeLeaseReentry {
155 holder_site: String,
156 requester_site: String,
157 },
158
159 #[error("invalid WAL ceiling {bytes} bytes: exceeds supported SQLite file offsets")]
162 WalCeilingOffsetOverflow { bytes: u64 },
163
164 #[error("invalid WAL ceiling {bytes} bytes: {backend_kind} does not support WAL enforcement")]
166 WalCeilingUnsupported {
167 bytes: u64,
168 backend_kind: &'static str,
169 },
170
171 #[error(
173 "invalid WAL ceiling {bytes} bytes: page size {page_size} requires at least {minimum_bytes} bytes for one WAL frame"
174 )]
175 WalCeilingBelowMinimum {
176 bytes: u64,
177 page_size: u64,
178 minimum_bytes: u64,
179 },
180
181 #[error(
183 "{stage}: WAL ceiling {bytes} bytes cannot be enforced: missing {capability}",
184 stage = SQLITE_WAL_CAPACITY_UNAVAILABLE_STAGE
185 )]
186 WalCapacityUnavailable {
187 bytes: u64,
188 capability: &'static str,
189 },
190
191 #[error("invalid config: {0}")]
196 InvalidConfig(String),
197
198 #[error("io error: {0}")]
200 Io(#[from] std::io::Error),
201
202 #[error("migration v{version} failed: {error}")]
204 Migration {
205 version: u32,
207 error: String,
209 },
210}
211
212impl SqliteError {
213 pub fn write(error: rusqlite::Error, stage: SqliteWriteStage) -> Self {
215 if matches!(
216 error,
217 rusqlite::Error::SqliteFailure(..) | rusqlite::Error::SqlInputError { .. }
218 ) {
219 Self::Write {
220 stage,
221 source: error,
222 }
223 } else {
224 Self::Rusqlite(error)
225 }
226 }
227
228 pub(crate) fn settlement_with_cause(
229 error: &(dyn std::error::Error + 'static),
230 stage: SqliteWriteStage,
231 ) -> Self {
232 match native_write_failure(error, stage) {
233 Some(mut failure) => {
234 failure.settlement_unknown = true;
235 Self::WriteSettlementUnknown { failure }
236 }
237 None => Self::WriterSettlementUnknown,
238 }
239 }
240
241 pub fn write_failure(&self) -> Option<SqliteWriteFailure> {
242 match self {
243 Self::Write { stage, source } => native_write_failure(source, *stage),
244 Self::WriteSettlementUnknown { failure } => Some(*failure),
245 _ => None,
246 }
247 }
248 pub fn wal_capacity_stage(&self) -> Option<&'static str> {
250 match self {
251 Self::WalCapacityUnavailable { .. } => Some(SQLITE_WAL_CAPACITY_UNAVAILABLE_STAGE),
252 _ => None,
253 }
254 }
255
256 pub fn into_storage_error(
262 self,
263 capability: StorageCapability,
264 operation: &'static str,
265 ) -> StorageError {
266 match self {
267 error @ Self::Write { stage, .. } => {
268 with_write_stage(StorageError::driver(capability, operation, error), stage)
269 }
270 Self::CapacityFloor {
271 volume,
272 available_bytes,
273 floor_bytes,
274 required_headroom_bytes,
275 } => StorageError::CapacityFloor {
276 capability: StorageCapability::Sql,
277 volume,
278 available_bytes,
279 floor_bytes,
280 required_headroom_bytes,
281 },
282 Self::CapacityUnavailable { phase, message } => StorageError::CapacityUnavailable {
283 capability: StorageCapability::Sql,
284 phase,
285 message,
286 },
287 Self::WriteSettlementUnknown { failure } => {
288 StorageError::writer_task_terminated(WriterTaskRequestState::SideEffectsUnknown)
289 .with_sqlite_write_failure(failure)
290 }
291 Self::InheritedWriterTransaction | Self::WriterSettlementUnknown => {
292 StorageError::writer_task_terminated(WriterTaskRequestState::SideEffectsUnknown)
293 }
294 Self::WriterPoisoned => {
295 StorageError::writer_task_terminated(WriterTaskRequestState::NotStarted)
296 }
297 other => StorageError::driver(capability, operation, other),
298 }
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305 use khive_storage::CapacityUnavailablePhase;
306
307 #[test]
308 fn capacity_and_settlement_errors_keep_their_meaning_from_any_store() {
309 for capability in [StorageCapability::Entities, StorageCapability::Sql] {
310 let refused = SqliteError::CapacityFloor {
311 volume: "/volume".to_string(),
312 available_bytes: 99,
313 floor_bytes: 100,
314 required_headroom_bytes: 12,
315 }
316 .into_storage_error(capability, "write");
317 assert!(
318 matches!(
319 refused,
320 StorageError::CapacityFloor {
321 capability: StorageCapability::Sql,
322 available_bytes: 99,
323 floor_bytes: 100,
324 required_headroom_bytes: 12,
325 ..
326 }
327 ),
328 "{capability:?}: {refused:?}"
329 );
330
331 let unavailable = SqliteError::CapacityUnavailable {
332 phase: CapacityUnavailablePhase::LockContended,
333 message: "lease timed out".to_string(),
334 }
335 .into_storage_error(capability, "write");
336 assert!(
337 matches!(
338 unavailable,
339 StorageError::CapacityUnavailable {
340 capability: StorageCapability::Sql,
341 phase: CapacityUnavailablePhase::LockContended,
342 ..
343 }
344 ),
345 "{capability:?}: {unavailable:?}"
346 );
347
348 for unsettled in [
349 SqliteError::InheritedWriterTransaction,
350 SqliteError::WriterSettlementUnknown,
351 ] {
352 let mapped = unsettled.into_storage_error(capability, "write");
353 assert!(
354 matches!(
355 mapped,
356 StorageError::WriterTaskTerminated {
357 request_state: WriterTaskRequestState::SideEffectsUnknown,
358 ..
359 }
360 ),
361 "{capability:?}: {mapped:?}"
362 );
363 }
364
365 let refused = SqliteError::WriterPoisoned.into_storage_error(capability, "write");
366 assert!(
367 matches!(
368 refused,
369 StorageError::WriterTaskTerminated {
370 request_state: WriterTaskRequestState::NotStarted,
371 ..
372 }
373 ),
374 "{capability:?}: {refused:?}"
375 );
376 }
377 }
378}