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,
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 cold store config: {message}")]
42 ColdStoreConfig { message: String },
43 #[error("invalid static Raft membership config: {message}")]
44 StaticMembershipConfig { message: String },
45 #[error("cold store IO error: {message}")]
46 ColdStoreIo { message: String },
47 #[error(
48 "core {} live read waiters at {current_waiters} would exceed limit {limit}",
49 .core_id.0
50 )]
51 LiveReadBackpressure {
52 core_id: CoreId,
53 current_waiters: u64,
54 limit: u64,
55 },
56 #[error("core {} does not host raft group {}", .core_id.0, .raft_group_id.0)]
57 GroupNotHosted {
58 core_id: CoreId,
59 raft_group_id: RaftGroupId,
60 },
61 #[error(
62 "core {} raft group {} operation failed: {}",
63 .core_id.0,
64 .raft_group_id.0,
65 .error.message()
66 )]
67 GroupEngine {
68 core_id: CoreId,
69 raft_group_id: RaftGroupId,
70 error: GroupEngineError,
71 },
72 #[error("core {} mailbox is closed", .core_id.0)]
73 MailboxClosed { core_id: CoreId },
74 #[error("core {} dropped append response", .core_id.0)]
75 ResponseDropped { core_id: CoreId },
76 #[error("failed to spawn core {} thread: {message}", .core_id.0)]
77 SpawnCoreThread { core_id: CoreId, message: String },
78}
79
80impl RuntimeError {
81 pub(crate) fn group_engine(placement: ShardPlacement, err: GroupEngineError) -> Self {
82 Self::GroupEngine {
83 core_id: placement.core_id,
84 raft_group_id: placement.raft_group_id,
85 error: err,
86 }
87 }
88
89 pub fn stream_error_code(&self) -> Option<StreamErrorCode> {
90 match self {
91 Self::GroupEngine { error, .. } => error.code(),
92 _ => None,
93 }
94 }
95
96 pub fn stream_next_offset(&self) -> Option<u64> {
97 match self {
98 Self::GroupEngine { error, .. } => error.next_offset(),
99 _ => None,
100 }
101 }
102
103 pub fn stream_error_context(&self) -> &[StreamErrorContext] {
104 match self {
105 Self::GroupEngine { error, .. } => error.context(),
106 _ => &[],
107 }
108 }
109
110 pub fn leader_hint(&self) -> Option<&GroupLeaderHint> {
111 match self {
112 Self::GroupEngine { error, .. } => error.leader_hint(),
113 _ => None,
114 }
115 }
116
117 pub fn status(&self) -> ErrorStatus {
118 match self {
119 Self::LiveReadBackpressure { .. } | Self::GroupNotHosted { .. } => {
120 ErrorStatus::Temporary
121 }
122 Self::GroupEngine { error, .. } if error.leader_hint().is_some() => {
123 ErrorStatus::Temporary
124 }
125 Self::GroupEngine { error, .. } if error.is_backpressure() => ErrorStatus::Temporary,
126 Self::GroupEngine { error, .. } if error.code().is_some() => ErrorStatus::Permanent,
127 Self::GroupEngine { .. } => ErrorStatus::Persistent,
128 _ => ErrorStatus::Permanent,
129 }
130 }
131}