1use crate::checkpoint::CheckpointMetadata;
2use crate::error::ErrorResponse;
3use actix_web::body::BoxBody;
4use actix_web::http::StatusCode;
5use actix_web::{HttpRequest, HttpResponse, HttpResponseBuilder, Responder, ResponseError};
6use bytemuck::NoUninit;
7use clap::ValueEnum;
8use serde::{Deserialize, Serialize};
9use serde_json::json;
10use std::fmt;
11use std::fmt::Display;
12use utoipa::ToSchema;
13
14#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, ToSchema, NoUninit)]
19#[cfg_attr(feature = "testing", derive(proptest_derive::Arbitrary))]
20#[repr(u8)]
21pub enum RuntimeStatus {
22 Unavailable,
32
33 Coordination,
36
37 Standby,
39
40 Initializing,
43
44 AwaitingApproval,
47
48 Bootstrapping,
51
52 Replaying,
55
56 Paused,
58
59 Running,
61
62 Suspended,
64
65 ConcurrentBootstrapping,
68
69 Synchronizing,
72}
73
74impl From<RuntimeDesiredStatus> for RuntimeStatus {
75 fn from(value: RuntimeDesiredStatus) -> Self {
76 match value {
77 RuntimeDesiredStatus::Unavailable => Self::Unavailable,
78 RuntimeDesiredStatus::Coordination => Self::Coordination,
79 RuntimeDesiredStatus::Standby => Self::Standby,
80 RuntimeDesiredStatus::Paused => Self::Paused,
81 RuntimeDesiredStatus::Running => Self::Running,
82 RuntimeDesiredStatus::Suspended => Self::Suspended,
83 }
84 }
85}
86
87#[derive(Debug, Clone, Copy, Eq, PartialEq, Deserialize, Serialize, ToSchema, ValueEnum)]
88#[cfg_attr(feature = "testing", derive(proptest_derive::Arbitrary))]
89pub enum RuntimeDesiredStatus {
90 Unavailable,
91 Coordination,
92 Standby,
93 Paused,
94 Running,
95 Suspended,
96}
97
98impl RuntimeDesiredStatus {
99 pub fn may_transition_to(&self, target: Self) -> bool {
100 match (*self, target) {
101 (old, new) if old == new => true,
102 (Self::Standby, Self::Paused | Self::Running) => true,
103 (Self::Paused, Self::Running | Self::Suspended) => true,
104 (Self::Running, Self::Paused | Self::Suspended) => true,
105 _ => false,
106 }
107 }
108
109 pub fn may_transition_to_at_startup(&self, target: Self) -> bool {
110 match (*self, target) {
111 (_, Self::Coordination) => true,
112 (Self::Suspended, _) => {
113 matches!(target, Self::Paused | Self::Running)
116 }
117 (old, new) if old.may_transition_to(new) => true,
118 _ => false,
119 }
120 }
121}
122
123pub mod snake_case_runtime_desired_status {
131 use serde::{Deserialize, Deserializer, Serialize, Serializer};
132
133 use crate::runtime_status::RuntimeDesiredStatus;
134
135 #[derive(Debug, Clone, Copy, Eq, PartialEq, Deserialize, Serialize)]
136 #[serde(rename_all = "snake_case")]
137 enum SnakeRuntimeDesiredStatus {
138 Unavailable,
139 Coordination,
140 Standby,
141 Paused,
142 Running,
143 Suspended,
144 }
145
146 impl From<RuntimeDesiredStatus> for SnakeRuntimeDesiredStatus {
147 fn from(value: RuntimeDesiredStatus) -> Self {
148 match value {
149 RuntimeDesiredStatus::Unavailable => SnakeRuntimeDesiredStatus::Unavailable,
150 RuntimeDesiredStatus::Coordination => SnakeRuntimeDesiredStatus::Coordination,
151 RuntimeDesiredStatus::Standby => SnakeRuntimeDesiredStatus::Standby,
152 RuntimeDesiredStatus::Paused => SnakeRuntimeDesiredStatus::Paused,
153 RuntimeDesiredStatus::Running => SnakeRuntimeDesiredStatus::Running,
154 RuntimeDesiredStatus::Suspended => SnakeRuntimeDesiredStatus::Suspended,
155 }
156 }
157 }
158
159 impl From<SnakeRuntimeDesiredStatus> for RuntimeDesiredStatus {
160 fn from(value: SnakeRuntimeDesiredStatus) -> Self {
161 match value {
162 SnakeRuntimeDesiredStatus::Unavailable => RuntimeDesiredStatus::Unavailable,
163 SnakeRuntimeDesiredStatus::Coordination => RuntimeDesiredStatus::Coordination,
164 SnakeRuntimeDesiredStatus::Standby => RuntimeDesiredStatus::Standby,
165 SnakeRuntimeDesiredStatus::Paused => RuntimeDesiredStatus::Paused,
166 SnakeRuntimeDesiredStatus::Running => RuntimeDesiredStatus::Running,
167 SnakeRuntimeDesiredStatus::Suspended => RuntimeDesiredStatus::Suspended,
168 }
169 }
170 }
171
172 pub fn serialize<S>(value: &RuntimeDesiredStatus, serializer: S) -> Result<S::Ok, S::Error>
173 where
174 S: Serializer,
175 {
176 SnakeRuntimeDesiredStatus::from(*value).serialize(serializer)
177 }
178
179 pub fn deserialize<'de, D>(deserializer: D) -> Result<RuntimeDesiredStatus, D::Error>
180 where
181 D: Deserializer<'de>,
182 {
183 SnakeRuntimeDesiredStatus::deserialize(deserializer).map(|status| status.into())
184 }
185}
186
187#[derive(
188 Debug, Default, Clone, Copy, Eq, PartialEq, Deserialize, Serialize, ToSchema, NoUninit,
189)]
190#[repr(u8)]
191#[serde(rename_all = "snake_case")]
192pub enum BootstrapPolicy {
193 Allow,
194 Reject,
195 #[default]
196 AwaitApproval,
197}
198
199impl TryFrom<Option<String>> for BootstrapPolicy {
200 type Error = ();
201
202 fn try_from(value: Option<String>) -> Result<Self, Self::Error> {
203 match value.as_deref() {
204 Some("allow") => Ok(Self::Allow),
205 Some("reject") => Ok(Self::Reject),
206 Some("await_approval") | None => Ok(Self::AwaitApproval),
207 _ => Err(()),
208 }
209 }
210}
211
212impl From<String> for BootstrapPolicy {
213 fn from(value: String) -> Self {
214 match value.as_str() {
215 "allow" => Self::Allow,
216 "reject" => Self::Reject,
217 "await_approval" => Self::AwaitApproval,
218 _ => panic!("Invalid 'bootstrap_policy' value: {value}"),
219 }
220 }
221}
222
223impl Display for BootstrapPolicy {
224 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
225 let s = match self {
226 BootstrapPolicy::Allow => "allow",
227 BootstrapPolicy::Reject => "reject",
228 BootstrapPolicy::AwaitApproval => "await_approval",
229 };
230 write!(f, "{s}")
231 }
232}
233
234#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, ToSchema, Default)]
236pub struct BootstrapConfig {
237 #[serde(default)]
239 pub bootstrap_policy: Option<BootstrapPolicy>,
240 #[serde(default)]
242 pub silent_bootstrap: bool,
243 #[serde(default)]
250 pub concurrent_bootstrap: bool,
251}
252
253impl From<BootstrapPolicy> for BootstrapConfig {
254 fn from(bootstrap_policy: BootstrapPolicy) -> Self {
255 Self {
256 bootstrap_policy: Some(bootstrap_policy),
257 silent_bootstrap: false,
258 concurrent_bootstrap: false,
259 }
260 }
261}
262
263impl BootstrapConfig {
264 pub fn with_silent_bootstrap(self, silent_bootstrap: bool) -> Self {
265 Self {
266 silent_bootstrap,
267 ..self
268 }
269 }
270
271 pub fn with_concurrent_bootstrap(self, concurrent_bootstrap: bool) -> Self {
272 Self {
273 concurrent_bootstrap,
274 ..self
275 }
276 }
277
278 pub fn validate(&self) -> Result<(), String> {
285 if self.silent_bootstrap && self.concurrent_bootstrap {
286 return Err(
287 "`silent_bootstrap` and `concurrent_bootstrap` are mutually exclusive; \
288 set at most one"
289 .to_string(),
290 );
291 }
292 Ok(())
293 }
294
295 pub fn active_bootstrap_policy(&self) -> BootstrapPolicy {
297 self.bootstrap_policy
298 .expect("bootstrap policy must be set for an active deployment")
299 }
300}
301
302#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, ToSchema)]
305pub struct StorageStatusDetails {
306 pub checkpoints: Vec<CheckpointMetadata>,
308}
309
310#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
311pub struct ExtendedRuntimeStatus {
312 pub runtime_status: RuntimeStatus,
314
315 pub runtime_status_details: serde_json::Value,
319
320 pub runtime_desired_status: RuntimeDesiredStatus,
322
323 pub storage_status_details: Option<StorageStatusDetails>,
329}
330
331impl Responder for ExtendedRuntimeStatus {
332 type Body = BoxBody;
333
334 fn respond_to(self, _req: &HttpRequest) -> HttpResponse<Self::Body> {
335 HttpResponseBuilder::new(StatusCode::OK).json(self)
336 }
337}
338
339impl From<ExtendedRuntimeStatus> for HttpResponse<BoxBody> {
340 fn from(value: ExtendedRuntimeStatus) -> Self {
341 HttpResponseBuilder::new(StatusCode::OK).json(value)
342 }
343}
344
345#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
347pub struct ExtendedRuntimeStatusError {
348 #[serde(with = "status_code")]
351 pub status_code: StatusCode,
352
353 pub error: ErrorResponse,
355}
356
357mod status_code {
358 use actix_web::http::StatusCode;
359 use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error};
360
361 pub fn serialize<S>(value: &StatusCode, serializer: S) -> Result<S::Ok, S::Error>
362 where
363 S: Serializer,
364 {
365 value.as_u16().serialize(serializer)
366 }
367
368 pub fn deserialize<'de, D>(deserializer: D) -> Result<StatusCode, D::Error>
369 where
370 D: Deserializer<'de>,
371 {
372 let value = u16::deserialize(deserializer)?;
373 StatusCode::from_u16(value).map_err(D::Error::custom)
374 }
375}
376
377impl Display for ExtendedRuntimeStatusError {
378 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
379 write!(f, "{}: {:?}", self.status_code, self.error)
380 }
381}
382
383impl ResponseError for ExtendedRuntimeStatusError {
384 fn status_code(&self) -> StatusCode {
385 self.status_code
386 }
387
388 fn error_response(&self) -> HttpResponse<BoxBody> {
389 HttpResponseBuilder::new(self.status_code()).json(self.error.clone())
390 }
391}
392
393#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Default, Eq, ToSchema)]
396pub struct RuntimeStatusDetails {
397 #[serde(skip_serializing_if = "Option::is_none")]
401 pub reason: Option<String>,
402
403 #[serde(skip_serializing_if = "Option::is_none")]
407 pub connector_stats: Option<ConnectorStats>,
408
409 #[serde(skip_serializing_if = "Option::is_none")]
413 pub approval_diff: Option<serde_json::Value>,
414 }
420
421impl RuntimeStatusDetails {
422 pub fn new_only_reason(reason: &str) -> Self {
423 Self {
424 reason: Some(reason.to_string()),
425 ..Self::default()
426 }
427 }
428
429 pub fn serialize_guaranteed(self) -> serde_json::Value {
434 serde_json::to_value(self).unwrap_or_else(|e| {
435 json!({
436 "reason": format!("unable to serialize runtime status details due to: {e}")
437 })
438 })
439 }
440}
441
442#[derive(Serialize, Deserialize, ToSchema, Eq, PartialEq, Debug, Clone)]
444pub struct ConnectorStats {
445 pub num_errors: u64,
452}