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 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}