nodedb_graph/error.rs
1// SPDX-License-Identifier: Apache-2.0
2
3//! Typed error enum for the shared graph engine.
4//!
5//! Every fallible operation on `CsrIndex` (label interning, edge insert,
6//! edge delete) returns `Result<T, GraphError>`. Silent casts or
7//! `debug_assert!` at capacity boundaries reproduce the same class of
8//! bug they are trying to catch — loud, typed errors only.
9
10use nodedb_mem::MemError;
11use thiserror::Error;
12
13/// Hard upper bound on the number of distinct edge labels an individual
14/// `CsrIndex` can intern. `u32::MAX` is the type-theoretic ceiling;
15/// leaving one slot unused lets callers use `u32::MAX` as an "invalid"
16/// sentinel should they need it.
17pub const MAX_EDGE_LABELS: usize = (u32::MAX - 1) as usize;
18
19/// Hard upper bound on the number of nodes a single `CsrIndex` partition
20/// can hold. Each node occupies a `u32` dense index; at 4 GiB nodes the
21/// index space is exhausted. `u32::MAX` is reserved as an "invalid"
22/// sentinel, so the usable cap is `u32::MAX - 1`.
23///
24/// 4 billion nodes per collection is well beyond any realistic workload.
25/// Inserts beyond this limit are rejected with `GraphError::NodeOverflow`
26/// rather than silently wrapping the counter.
27pub const MAX_NODES_PER_CSR: usize = (u32::MAX - 1) as usize;
28
29/// Errors returned by graph-engine operations.
30#[derive(Debug, Error)]
31#[non_exhaustive]
32pub enum GraphError {
33 /// The CSR's edge-label id space is exhausted. Happens only when
34 /// more than `MAX_EDGE_LABELS` distinct labels have been interned
35 /// — in practice unreachable because the DSL ingress caps label
36 /// length and realistic workloads use orders of magnitude fewer
37 /// labels. Surfaced here so the failure mode is a typed error, not
38 /// a silent wrap (the bug this crate was shipping before).
39 #[error("CSR edge-label id space exhausted ({used}/{MAX_EDGE_LABELS} labels interned)")]
40 LabelOverflow { used: usize },
41
42 /// The CSR's node id space is exhausted. A single `CsrIndex` partition
43 /// supports at most `MAX_NODES_PER_CSR` (≈ 4 billion) distinct nodes.
44 /// Inserts beyond this limit are rejected rather than silently wrapping
45 /// the `u32` node index. Collections approaching this limit should be
46 /// sharded across multiple partitions.
47 #[error(
48 "CSR node id space exhausted ({used}/{MAX_NODES_PER_CSR} nodes); \
49 collection must be sharded to accept more nodes"
50 )]
51 NodeOverflow { used: usize },
52
53 /// The memory governor rejected a reservation for a graph operation.
54 ///
55 /// Callers should apply backpressure and retry after memory is released.
56 #[error("graph memory budget rejected: {0}")]
57 MemoryBudget(#[from] MemError),
58
59 /// A rollback asked to withdraw an interned node or node label that is
60 /// not the newest one, or that something still refers to. Withdrawing it
61 /// would renumber or orphan live state.
62 #[error("cannot withdraw {kind} '{name}': it is not the newest {kind} or it is still in use")]
63 WithdrawRefused { kind: &'static str, name: String },
64
65 /// A rebuild of this index is already running. The partition holds one
66 /// write journal at a time.
67 #[error("a rebuild of this CSR index is already running")]
68 RebuildInProgress,
69
70 /// The index a rebuild started from is no longer the live one: it was
71 /// dropped, replaced, or its rebuild was aborted. The rebuilt copy is
72 /// discarded and the live index stays as it is.
73 #[error("the CSR index this rebuild started from is no longer live; rebuild discarded")]
74 RebuildSuperseded,
75
76 /// The writes made during a rebuild exceeded the journal bound. The
77 /// rebuilt copy is discarded, the live index keeps every write, and a
78 /// new rebuild can run.
79 #[error(
80 "CSR writes during the rebuild exceeded the {cap_bytes}-byte journal bound; \
81 rebuild discarded, live index unchanged; run REINDEX again"
82 )]
83 RebuildJournalOverflow { cap_bytes: usize },
84
85 /// A journaled write returned a different outcome on the rebuilt index
86 /// than on the live one. The rebuilt copy is discarded.
87 #[error("CSR rebuild replay of '{op}' diverged from the live index; rebuild discarded")]
88 RebuildReplayDiverged { op: &'static str },
89
90 /// The snapshot a rebuild carries does not decode into an index.
91 #[error("CSR rebuild snapshot is invalid: {detail}")]
92 RebuildSnapshotInvalid { detail: String },
93}