pub enum StoreError {
Show 18 variants
Open {
path: PathBuf,
source: Error,
},
Sqlite(Error),
SchemaTooNew {
found: u32,
supported: u32,
},
Migration {
version: u32,
name: &'static str,
source: Error,
},
StaleRevision {
id: PolicyId,
expected: u64,
found: u64,
},
ActiveCountChanged {
id: PolicyId,
expected: u16,
found: u16,
},
RunnerRootChanged {
id: HostId,
expected: String,
found: String,
},
UncleanedCountChanged {
subject: String,
expected: u16,
found: u16,
},
SlotAlreadyLeased {
policy: PolicyId,
slot: u16,
},
NotFound {
what: &'static str,
id: String,
},
AlreadyExists {
what: &'static str,
id: String,
},
CorruptPolicy {
id: PolicyId,
source: PolicyError,
},
CorruptAttempt {
id: AttemptId,
source: AttemptError,
},
CorruptHost {
id: HostId,
source: ValidationError,
},
CorruptHostWorkspace {
id: HostId,
source: WorkspaceError,
},
CorruptColumn {
table: &'static str,
column: &'static str,
id: String,
value: String,
expected: &'static str,
},
UnrepresentableInteger {
what: &'static str,
value: u64,
},
UnrepresentablePath {
attempt: AttemptId,
path: PathBuf,
},
}Expand description
Everything that can go wrong between a domain value and a database row.
The variants are split the way a caller has to branch, not the way the
implementation happens to fail. In particular StoreError::StaleRevision is
its own variant rather than a flavour of StoreError::Sqlite, because a
concurrent edit is an ordinary outcome the CLI and the TUI must report as
“someone else changed this; re-read and try again”, while an I/O failure is
not. StoreError::is_conflict is the predicate for that branch.
Variants§
Open
The database file could not be opened. The parent directory belongs to
d1; this module never creates it.
Sqlite(Error)
Any other SQLite failure: a real I/O error, a locked database, a malformed file.
SchemaTooNew
The database was written by a newer build of this product.
This fails closed on purpose. A newer version may have added a column this build does not write — which this build would then drop on its next write — or changed the meaning of one it does. Guessing is how a downgrade silently corrupts a configuration.
Migration
A migration did not apply. The transaction around it rolled back, so the database is still at the previous version.
StaleRevision
A write lost an optimistic-concurrency race. Nothing was written.
The caller must re-read the policy and re-apply its change; it must not retry the value it holds, because that value was derived from a revision that no longer exists.
ActiveCountChanged
Active work changed after an operator observed it for a disable. The policy update and this predicate execute under one SQLite write transaction, so no attempt journal write can cross the check.
RunnerRootChanged
A host runner-root write was built from an override that is no longer the stored one. Nothing was written.
This is the host counterpart of StoreError::StaleRevision, and it
exists because hosts carries no revision column: 03-migration-rollout
requires the host mutation to compare “the expected old override” and
update only that column, so that a capacity or service-mode change made
between the operator’s read and this write is not silently rolled back by
a whole-record Store::put_host.
Both paths are rendered rather than optional, so a message reads the same way whichever direction the change went: an unset override is “the platform default”.
UncleanedCountChanged
Uncleaned attempts changed after an operator observed the count a path mutation was refused or permitted on. Nothing was written.
Distinct from StoreError::ActiveCountChanged, and the difference is
the whole point of the variant. Active excludes a terminal attempt;
uncleaned includes one, because a finished attempt whose cleanup has
not run still owns the directory under the root being moved, and a
persistent one still holds its slot lease
(04-security-recovery.md: “A host root setting cannot change while any
ephemeral attempt is active or unresolved”).
subject names the host or policy the count was taken for, already
rendered, because the two callers count different sets and an operator
reading the message needs to know which.
SlotAlreadyLeased
Two uncleaned persistent attempts cannot hold one slot.
Raised when a journal write collides with the partial unique index
one_uncleaned_persistent_attempt_per_slot. The allocation lock in c2
coordinates slot selection; this is the durable guard that catches the
race the lock cannot see — a second process, or a restart that lost the
lock — and 04-security-recovery.md names it as the control for “two
attempts use one slot concurrently”.
Nothing was written: the statement is a single INSERT, so SQLite rolls it back whole and the existing lease is untouched.
NotFound
The row a write was aimed at is not there.
AlreadyExists
An insert collided with an existing primary key.
CorruptPolicy
A stored policy is not a legal policy. This is the hand-edited-database
case: D19’s shape rules and min <= max are re-run on every load.
CorruptAttempt
A stored attempt is not a legal attempt: its state, outcome and timestamps do not pair the way this crate’s own transitions pair them.
CorruptHost
A stored host does not satisfy a domain constraint — a blank display name, or a refresh interval under the documented floor.
CorruptHostWorkspace
A stored host’s configured runner root is not a shape this product will place a runner under.
Separate from StoreError::CorruptHost because the source error is a
different vocabulary: hosts.runner_root_override is re-parsed through
LocalAbsolutePath::new, so a hand-edited \\nas\builds, a relative
path, a bare drive root, or a Windows path in a database opened on Linux
fails closed here with the reason attached (D10). A path is not a
credential — see crates/domain/src/path.rs — so the offending text may
travel in the message, which is what makes the refusal actionable.
CorruptColumn
One column holds something that is not the kind of value it is declared to hold. The row is named so an operator can find and fix it.
value never repeats the whole payload, and how much it repeats
depends on which column this is. The row id is what an operator needs
to find the row; the payload only helps them recognise it, and repeating
all of it turns this error into a disclosure the moment it reaches a log.
table and column are carried for the operator’s sake and are also
what decides the echo: a column whose shape the schema fixes gets a
clipped echo of at most ECHO_LIMIT characters, and one that may hold
text the agent captured from a failure gets position only, with none of
the payload. The rule and the measurement behind it are on
FREE_FORM_COLUMNS, beside the decoder that applies it. (Named rather
than linked: it is private, and a link from here would not resolve for a
reader of the public docs.)
Fields
value: StringAt most ECHO_LIMIT characters of the offending payload for a
constrained column, and none of it for a free-form one.
UnrepresentableInteger
An integer that does not fit in a SQLite integer.
SQLite has no unsigned 64-bit type, so a u64 above i64::MAX has no
representation. Refused rather than saturated: saturating stores one
number and reads a different one back, silently, and the two values the
domain carries as u64 – installation_id and github_runner_id –
both come from GitHub, so a caller can reach this without doing anything
unusual.
UnrepresentablePath
A runtime path that is not valid UTF-8 and therefore cannot be stored as text.
Lossy conversion is deliberately not used: e3 deletes the runtime
directory this path names, and a path mangled by U+FFFD substitution
either fails to delete or names a different directory.
Implementations§
Source§impl StoreError
impl StoreError
Sourcepub const fn is_conflict(&self) -> bool
pub const fn is_conflict(&self) -> bool
Whether this is an optimistic-concurrency conflict rather than a failure.
The Definition of Done requires that “a stale-revision write is rejected
and the caller can distinguish it from an I/O error”. This is that
distinction, exposed so a caller need not match on the variant shape to
make it.
The three fences the workspace mutations add are conflicts on exactly the
same footing: each means “someone else got there first, re-read and try
again”, and none of them means the database is unwell.
StoreError::SlotAlreadyLeased is included because that is what an
allocator that lost a race to a slot must do — pick another one — rather
than surface an I/O failure to an operator.
Trait Implementations§
Source§impl Debug for StoreError
impl Debug for StoreError
Source§impl Display for StoreError
impl Display for StoreError
Source§impl Error for StoreError
impl Error for StoreError
Source§fn source(&self) -> Option<&(dyn Error + 'static)>
fn source(&self) -> Option<&(dyn Error + 'static)>
1.0.0 · Source§fn description(&self) -> &str
fn description(&self) -> &str
use the Display impl or to_string()