1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
//! Error types for the SQLite storage layer.
use std::time::Duration;
use khive_storage::{StorageCapability, StorageError, WriterTaskRequestState};
use thiserror::Error;
/// Stable ADR-194 capacity stages. The refusal stage is reserved for the WAL
/// I/O limiter; this configuration-only slice emits only unavailable.
pub const SQLITE_WAL_CAPACITY_REFUSED_STAGE: &str = "sqlite_wal_capacity_refused";
pub const SQLITE_WAL_CAPACITY_UNAVAILABLE_STAGE: &str = "sqlite_wal_capacity_unavailable";
/// Errors produced by the SQLite storage backend.
#[derive(Debug, Error)]
pub enum SqliteError {
/// A request-scoped read or store acquisition stopped, or read cleanup failed.
#[error(transparent)]
RequestReadStopped(khive_storage::StorageError),
/// Underlying rusqlite driver error.
#[error("sqlite error: {0}")]
Rusqlite(#[from] rusqlite::Error),
/// Data invariant violation (corrupt row, unexpected schema state).
#[error("invalid data: {0}")]
InvalidData(String),
/// A pooled connection contained a transaction from an earlier owner.
/// Its prior side effects cannot be attributed to the new request.
#[error("pooled writer contains an inherited transaction; prior side effects are unknown")]
InheritedWriterTransaction,
/// The writer could not prove transaction settlement before retirement.
#[error("writer transaction settlement is unknown; connection retired")]
WriterSettlementUnknown,
/// An earlier write on this database could not prove its settlement, so
/// every later write is refused before it starts. Only the write whose
/// settlement failed reports an unknown outcome; this one never ran.
#[error("writer refused: an earlier write's settlement is unknown; this write did not start")]
WriterPoisoned,
/// The process-local writer mutex was not acquired within the pool's
/// configured finite checkout deadline. This stage happens before SQLite
/// executes, so callers must not conflate it with SQLite busy/locked or
/// checkpoint starvation.
///
/// The display text intentionally retains the historical `InvalidData`
/// prefix for compatibility while the variant supplies stable structural
/// classification (ADR-135 F6).
#[error("invalid data: timed out after {timeout:?} waiting for sqlite writer connection")]
WriterPoolCheckoutTimeout {
/// Pool checkout deadline that elapsed.
timeout: Duration,
},
/// A file-backed writer was refused before SQLite began the operation
/// because the volume's free space had reached its configured reserve.
#[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,
},
/// A new logical write could not resolve its volume, acquire its lease,
/// or sample available space.
#[error("sqlite capacity admission unavailable in {phase} phase: {message}")]
CapacityUnavailable {
phase: khive_storage::CapacityUnavailablePhase,
message: String,
},
/// The thread asking for a volume's write lease already holds it, so
/// waiting could never succeed. This is a nested write, not lock
/// contention, and it is refused at once instead of at the deadline.
#[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,
},
/// A configured WAL ceiling cannot be represented by SQLite's signed
/// file-offset arithmetic.
#[error("invalid WAL ceiling {bytes} bytes: exceeds supported SQLite file offsets")]
WalCeilingOffsetOverflow { bytes: u64 },
/// A WAL ceiling was enabled for a backend that cannot produce a WAL.
#[error("invalid WAL ceiling {bytes} bytes: {backend_kind} does not support WAL enforcement")]
WalCeilingUnsupported {
bytes: u64,
backend_kind: &'static str,
},
/// One committed WAL frame cannot fit, even immediately after reset.
#[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,
},
/// A valid enabled policy cannot run until its WAL I/O limiter exists.
#[error(
"{stage}: WAL ceiling {bytes} bytes cannot be enforced: missing {capability}",
stage = SQLITE_WAL_CAPACITY_UNAVAILABLE_STAGE
)]
WalCapacityUnavailable {
bytes: u64,
capability: &'static str,
},
/// A `PoolConfig` value violated a validated invariant at configuration
/// load time (e.g. ADR-131 Decision 2's `write_admission_deadline_ms`
/// range). Fires before any connection is opened, and is never silently
/// clamped into range.
#[error("invalid config: {0}")]
InvalidConfig(String),
/// Filesystem I/O error.
#[error("io error: {0}")]
Io(#[from] std::io::Error),
/// A versioned migration failed to apply.
#[error("migration v{version} failed: {error}")]
Migration {
/// The migration version number that failed.
version: u32,
/// Human-readable description of the failure.
error: String,
},
}
impl SqliteError {
/// Stable structured stage for an ADR-194 WAL-capacity failure.
pub fn wal_capacity_stage(&self) -> Option<&'static str> {
match self {
Self::WalCapacityUnavailable { .. } => Some(SQLITE_WAL_CAPACITY_UNAVAILABLE_STAGE),
_ => None,
}
}
/// Capacity admission is a property of the SQLite file, so its refusals
/// carry `StorageCapability::Sql` whichever store requested the write.
pub(crate) fn into_storage_error(
self,
capability: StorageCapability,
operation: &'static str,
) -> StorageError {
match self {
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::InheritedWriterTransaction | Self::WriterSettlementUnknown => {
StorageError::WriterTaskTerminated {
request_state: WriterTaskRequestState::SideEffectsUnknown,
}
}
Self::WriterPoisoned => StorageError::WriterTaskTerminated {
request_state: 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::Lock,
message: "lease timed out".to_string(),
}
.into_storage_error(capability, "write");
assert!(
matches!(
unavailable,
StorageError::CapacityUnavailable {
capability: StorageCapability::Sql,
phase: CapacityUnavailablePhase::Lock,
..
}
),
"{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:?}"
);
}
}
}