Skip to main content

ursula_runtime/
error.rs

1use ursula_shard::CoreId;
2use ursula_shard::RaftGroupId;
3use ursula_shard::ShardMapError;
4use ursula_shard::ShardPlacement;
5use ursula_stream::StreamErrorCode;
6use ursula_stream::StreamErrorContext;
7
8use crate::engine::GroupEngineError;
9use crate::engine::GroupLeaderHint;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum ErrorStatus {
13    Permanent,
14    Temporary,
15    // Persistent is reserved for non-retryable service-side failures. HTTP currently
16    // treats it like Permanent; keeping it distinct leaves room for logging/alerting.
17    Persistent,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
21pub enum RuntimeError {
22    #[error("invalid shard runtime config: {0}")]
23    InvalidConfig(#[from] ShardMapError),
24    #[error("raft group {} is outside configured range 0..{raft_group_count}", .raft_group_id.0)]
25    InvalidRaftGroup {
26        raft_group_id: RaftGroupId,
27        raft_group_count: u32,
28    },
29    #[error(
30        "snapshot placement for raft group {} is core {}, expected core {}",
31        .actual.raft_group_id.0,
32        .actual.core_id.0,
33        .expected.core_id.0
34    )]
35    SnapshotPlacementMismatch {
36        expected: ShardPlacement,
37        actual: ShardPlacement,
38    },
39    #[error("append payload must be non-empty")]
40    EmptyAppend,
41    #[error("invalid append transaction: {message}")]
42    InvalidAppendTransaction { message: String },
43    #[error("invalid cold store config: {message}")]
44    ColdStoreConfig { message: String },
45    #[error("invalid static Raft membership config: {message}")]
46    StaticMembershipConfig { message: String },
47    #[error("cold store IO error: {message}")]
48    ColdStoreIo { message: String },
49    #[error(
50        "core {} live read waiters at {current_waiters} would exceed limit {limit}",
51        .core_id.0
52    )]
53    LiveReadBackpressure {
54        core_id: CoreId,
55        current_waiters: u64,
56        limit: u64,
57    },
58    #[error("core {} does not host raft group {}", .core_id.0, .raft_group_id.0)]
59    GroupNotHosted {
60        core_id: CoreId,
61        raft_group_id: RaftGroupId,
62    },
63    #[error(
64        "core {} raft group {} operation failed: {}",
65        .core_id.0,
66        .raft_group_id.0,
67        .error.message()
68    )]
69    GroupEngine {
70        core_id: CoreId,
71        raft_group_id: RaftGroupId,
72        error: GroupEngineError,
73    },
74    #[error("core {} mailbox is closed", .core_id.0)]
75    MailboxClosed { core_id: CoreId },
76    #[error("core {} dropped append response", .core_id.0)]
77    ResponseDropped { core_id: CoreId },
78    #[error("failed to spawn core {} thread: {message}", .core_id.0)]
79    SpawnCoreThread { core_id: CoreId, message: String },
80}
81
82impl RuntimeError {
83    pub(crate) fn group_engine(placement: ShardPlacement, err: GroupEngineError) -> Self {
84        Self::GroupEngine {
85            core_id: placement.core_id,
86            raft_group_id: placement.raft_group_id,
87            error: err,
88        }
89    }
90
91    pub fn stream_error_code(&self) -> Option<StreamErrorCode> {
92        match self {
93            Self::GroupEngine { error, .. } => error.code(),
94            _ => None,
95        }
96    }
97
98    pub fn stream_next_offset(&self) -> Option<u64> {
99        match self {
100            Self::GroupEngine { error, .. } => error.next_offset(),
101            _ => None,
102        }
103    }
104
105    pub fn stream_error_context(&self) -> &[StreamErrorContext] {
106        match self {
107            Self::GroupEngine { error, .. } => error.context(),
108            _ => &[],
109        }
110    }
111
112    pub fn leader_hint(&self) -> Option<&GroupLeaderHint> {
113        match self {
114            Self::GroupEngine { error, .. } => error.leader_hint(),
115            _ => None,
116        }
117    }
118
119    pub fn status(&self) -> ErrorStatus {
120        match self {
121            Self::LiveReadBackpressure { .. } | Self::GroupNotHosted { .. } => {
122                ErrorStatus::Temporary
123            }
124            Self::GroupEngine { error, .. } if error.leader_hint().is_some() => {
125                ErrorStatus::Temporary
126            }
127            Self::GroupEngine { error, .. } if error.is_backpressure() => ErrorStatus::Temporary,
128            Self::GroupEngine { error, .. } if error.code().is_some() => ErrorStatus::Permanent,
129            Self::GroupEngine { .. } => ErrorStatus::Persistent,
130            _ => ErrorStatus::Permanent,
131        }
132    }
133}