Line data Source code
1 : pub mod partitioning;
2 :
3 : use std::{
4 : collections::HashMap,
5 : io::{BufRead, Read},
6 : num::{NonZeroU64, NonZeroUsize},
7 : time::{Duration, SystemTime},
8 : };
9 :
10 : use byteorder::{BigEndian, ReadBytesExt};
11 : use postgres_ffi::BLCKSZ;
12 : use serde::{Deserialize, Serialize};
13 : use serde_with::serde_as;
14 : use strum_macros;
15 : use utils::{
16 : completion,
17 : history_buffer::HistoryBufferWithDropCounter,
18 : id::{NodeId, TenantId, TimelineId},
19 : lsn::Lsn,
20 : };
21 :
22 : use crate::{
23 : reltag::RelTag,
24 : shard::{ShardCount, ShardStripeSize, TenantShardId},
25 : };
26 : use anyhow::bail;
27 : use bytes::{Buf, BufMut, Bytes, BytesMut};
28 :
29 : /// The state of a tenant in this pageserver.
30 : ///
31 : /// ```mermaid
32 : /// stateDiagram-v2
33 : ///
34 : /// [*] --> Loading: spawn_load()
35 : /// [*] --> Attaching: spawn_attach()
36 : ///
37 : /// Loading --> Activating: activate()
38 : /// Attaching --> Activating: activate()
39 : /// Activating --> Active: infallible
40 : ///
41 : /// Loading --> Broken: load() failure
42 : /// Attaching --> Broken: attach() failure
43 : ///
44 : /// Active --> Stopping: set_stopping(), part of shutdown & detach
45 : /// Stopping --> Broken: late error in remove_tenant_from_memory
46 : ///
47 : /// Broken --> [*]: ignore / detach / shutdown
48 : /// Stopping --> [*]: remove_from_memory complete
49 : ///
50 : /// Active --> Broken: cfg(testing)-only tenant break point
51 : /// ```
52 : #[derive(
53 36831 : Clone,
54 5065 : PartialEq,
55 : Eq,
56 892 : serde::Serialize,
57 34 : serde::Deserialize,
58 19 : strum_macros::Display,
59 : strum_macros::EnumVariantNames,
60 0 : strum_macros::AsRefStr,
61 3555 : strum_macros::IntoStaticStr,
62 : )]
63 : #[serde(tag = "slug", content = "data")]
64 : pub enum TenantState {
65 : /// This tenant is being loaded from local disk.
66 : ///
67 : /// `set_stopping()` and `set_broken()` do not work in this state and wait for it to pass.
68 : Loading,
69 : /// This tenant is being attached to the pageserver.
70 : ///
71 : /// `set_stopping()` and `set_broken()` do not work in this state and wait for it to pass.
72 : Attaching,
73 : /// The tenant is transitioning from Loading/Attaching to Active.
74 : ///
75 : /// While in this state, the individual timelines are being activated.
76 : ///
77 : /// `set_stopping()` and `set_broken()` do not work in this state and wait for it to pass.
78 : Activating(ActivatingFrom),
79 : /// The tenant has finished activating and is open for business.
80 : ///
81 : /// Transitions out of this state are possible through `set_stopping()` and `set_broken()`.
82 : Active,
83 : /// The tenant is recognized by pageserver, but it is being detached or the
84 : /// system is being shut down.
85 : ///
86 : /// Transitions out of this state are possible through `set_broken()`.
87 : Stopping {
88 : // Because of https://github.com/serde-rs/serde/issues/2105 this has to be a named field,
89 : // otherwise it will not be skipped during deserialization
90 : #[serde(skip)]
91 : progress: completion::Barrier,
92 : },
93 : /// The tenant is recognized by the pageserver, but can no longer be used for
94 : /// any operations.
95 : ///
96 : /// If the tenant fails to load or attach, it will transition to this state
97 : /// and it is guaranteed that no background tasks are running in its name.
98 : ///
99 : /// The other way to transition into this state is from `Stopping` state
100 : /// through `set_broken()` called from `remove_tenant_from_memory()`. That happens
101 : /// if the cleanup future executed by `remove_tenant_from_memory()` fails.
102 : Broken { reason: String, backtrace: String },
103 : }
104 :
105 : impl TenantState {
106 660 : pub fn attachment_status(&self) -> TenantAttachmentStatus {
107 : use TenantAttachmentStatus::*;
108 :
109 : // Below TenantState::Activating is used as "transient" or "transparent" state for
110 : // attachment_status determining.
111 0 : match self {
112 : // The attach procedure writes the marker file before adding the Attaching tenant to the tenants map.
113 : // So, technically, we can return Attached here.
114 : // However, as soon as Console observes Attached, it will proceed with the Postgres-level health check.
115 : // But, our attach task might still be fetching the remote timelines, etc.
116 : // So, return `Maybe` while Attaching, making Console wait for the attach task to finish.
117 216 : Self::Attaching | Self::Activating(ActivatingFrom::Attaching) => Maybe,
118 : // tenant mgr startup distinguishes attaching from loading via marker file.
119 0 : Self::Loading | Self::Activating(ActivatingFrom::Loading) => Attached,
120 : // We only reach Active after successful load / attach.
121 : // So, call atttachment status Attached.
122 222 : Self::Active => Attached,
123 : // If the (initial or resumed) attach procedure fails, the tenant becomes Broken.
124 : // However, it also becomes Broken if the regular load fails.
125 : // From Console's perspective there's no practical difference
126 : // because attachment_status is polled by console only during attach operation execution.
127 103 : Self::Broken { reason, .. } => Failed {
128 103 : reason: reason.to_owned(),
129 103 : },
130 : // Why is Stopping a Maybe case? Because, during pageserver shutdown,
131 : // we set the Stopping state irrespective of whether the tenant
132 : // has finished attaching or not.
133 119 : Self::Stopping { .. } => Maybe,
134 : }
135 660 : }
136 :
137 58 : pub fn broken_from_reason(reason: String) -> Self {
138 58 : let backtrace_str: String = format!("{}", std::backtrace::Backtrace::force_capture());
139 58 : Self::Broken {
140 58 : reason,
141 58 : backtrace: backtrace_str,
142 58 : }
143 58 : }
144 : }
145 :
146 : impl std::fmt::Debug for TenantState {
147 16 : fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148 4 : match self {
149 4 : Self::Broken { reason, backtrace } if !reason.is_empty() => {
150 4 : write!(f, "Broken due to: {reason}. Backtrace:\n{backtrace}")
151 : }
152 12 : _ => write!(f, "{self}"),
153 : }
154 16 : }
155 : }
156 :
157 : /// The only [`TenantState`] variants we could be `TenantState::Activating` from.
158 594 : #[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
159 : pub enum ActivatingFrom {
160 : /// Arrived to [`TenantState::Activating`] from [`TenantState::Loading`]
161 : Loading,
162 : /// Arrived to [`TenantState::Activating`] from [`TenantState::Attaching`]
163 : Attaching,
164 : }
165 :
166 : /// A state of a timeline in pageserver's memory.
167 3577248 : #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
168 : pub enum TimelineState {
169 : /// The timeline is recognized by the pageserver but is not yet operational.
170 : /// In particular, the walreceiver connection loop is not running for this timeline.
171 : /// It will eventually transition to state Active or Broken.
172 : Loading,
173 : /// The timeline is fully operational.
174 : /// It can be queried, and the walreceiver connection loop is running.
175 : Active,
176 : /// The timeline was previously Loading or Active but is shutting down.
177 : /// It cannot transition back into any other state.
178 : Stopping,
179 : /// The timeline is broken and not operational (previous states: Loading or Active).
180 : Broken { reason: String, backtrace: String },
181 : }
182 :
183 18575 : #[derive(Serialize, Deserialize)]
184 : pub struct TimelineCreateRequest {
185 : pub new_timeline_id: TimelineId,
186 : #[serde(default)]
187 : pub ancestor_timeline_id: Option<TimelineId>,
188 : #[serde(default)]
189 : pub existing_initdb_timeline_id: Option<TimelineId>,
190 : #[serde(default)]
191 : pub ancestor_start_lsn: Option<Lsn>,
192 : pub pg_version: Option<u32>,
193 : }
194 :
195 21 : #[derive(Serialize, Deserialize)]
196 : pub struct TenantShardSplitRequest {
197 : pub new_shard_count: u8,
198 : }
199 :
200 15 : #[derive(Serialize, Deserialize)]
201 : pub struct TenantShardSplitResponse {
202 : pub new_shards: Vec<TenantShardId>,
203 : }
204 :
205 : /// Parameters that apply to all shards in a tenant. Used during tenant creation.
206 2950 : #[derive(Serialize, Deserialize, Debug)]
207 : #[serde(deny_unknown_fields)]
208 : pub struct ShardParameters {
209 : pub count: ShardCount,
210 : pub stripe_size: ShardStripeSize,
211 : }
212 :
213 : impl ShardParameters {
214 : pub const DEFAULT_STRIPE_SIZE: ShardStripeSize = ShardStripeSize(256 * 1024 / 8);
215 :
216 920 : pub fn is_unsharded(&self) -> bool {
217 920 : self.count == ShardCount(0)
218 920 : }
219 : }
220 :
221 : impl Default for ShardParameters {
222 733 : fn default() -> Self {
223 733 : Self {
224 733 : count: ShardCount(0),
225 733 : stripe_size: Self::DEFAULT_STRIPE_SIZE,
226 733 : }
227 733 : }
228 : }
229 :
230 11406 : #[derive(Serialize, Deserialize, Debug)]
231 : #[serde(deny_unknown_fields)]
232 : pub struct TenantCreateRequest {
233 : pub new_tenant_id: TenantShardId,
234 : #[serde(default)]
235 : #[serde(skip_serializing_if = "Option::is_none")]
236 : pub generation: Option<u32>,
237 :
238 : // If omitted, create a single shard with TenantShardId::unsharded()
239 : #[serde(default)]
240 : #[serde(skip_serializing_if = "ShardParameters::is_unsharded")]
241 : pub shard_parameters: ShardParameters,
242 :
243 : #[serde(flatten)]
244 : pub config: TenantConfig, // as we have a flattened field, we should reject all unknown fields in it
245 : }
246 :
247 12 : #[derive(Deserialize, Debug)]
248 : #[serde(deny_unknown_fields)]
249 : pub struct TenantLoadRequest {
250 : #[serde(default)]
251 : #[serde(skip_serializing_if = "Option::is_none")]
252 : pub generation: Option<u32>,
253 : }
254 :
255 : impl std::ops::Deref for TenantCreateRequest {
256 : type Target = TenantConfig;
257 :
258 0 : fn deref(&self) -> &Self::Target {
259 0 : &self.config
260 0 : }
261 : }
262 :
263 : /// An alternative representation of `pageserver::tenant::TenantConf` with
264 : /// simpler types.
265 39818 : #[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)]
266 : pub struct TenantConfig {
267 : pub checkpoint_distance: Option<u64>,
268 : pub checkpoint_timeout: Option<String>,
269 : pub compaction_target_size: Option<u64>,
270 : pub compaction_period: Option<String>,
271 : pub compaction_threshold: Option<usize>,
272 : pub gc_horizon: Option<u64>,
273 : pub gc_period: Option<String>,
274 : pub image_creation_threshold: Option<usize>,
275 : pub pitr_interval: Option<String>,
276 : pub walreceiver_connect_timeout: Option<String>,
277 : pub lagging_wal_timeout: Option<String>,
278 : pub max_lsn_wal_lag: Option<NonZeroU64>,
279 : pub trace_read_requests: Option<bool>,
280 : pub eviction_policy: Option<EvictionPolicy>,
281 : pub min_resident_size_override: Option<u64>,
282 : pub evictions_low_residence_duration_metric_threshold: Option<String>,
283 : pub gc_feedback: Option<bool>,
284 : pub heatmap_period: Option<String>,
285 : pub lazy_slru_download: Option<bool>,
286 : }
287 :
288 80 : #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
289 : #[serde(tag = "kind")]
290 : pub enum EvictionPolicy {
291 : NoEviction,
292 : LayerAccessThreshold(EvictionPolicyLayerAccessThreshold),
293 : }
294 :
295 : impl EvictionPolicy {
296 1346 : pub fn discriminant_str(&self) -> &'static str {
297 1346 : match self {
298 1315 : EvictionPolicy::NoEviction => "NoEviction",
299 31 : EvictionPolicy::LayerAccessThreshold(_) => "LayerAccessThreshold",
300 : }
301 1346 : }
302 : }
303 :
304 133 : #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
305 : pub struct EvictionPolicyLayerAccessThreshold {
306 : #[serde(with = "humantime_serde")]
307 : pub period: Duration,
308 : #[serde(with = "humantime_serde")]
309 : pub threshold: Duration,
310 : }
311 :
312 : /// A flattened analog of a `pagesever::tenant::LocationMode`, which
313 : /// lists out all possible states (and the virtual "Detached" state)
314 : /// in a flat form rather than using rust-style enums.
315 1546 : #[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
316 : pub enum LocationConfigMode {
317 : AttachedSingle,
318 : AttachedMulti,
319 : AttachedStale,
320 : Secondary,
321 : Detached,
322 : }
323 :
324 102 : #[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
325 : pub struct LocationConfigSecondary {
326 : pub warm: bool,
327 : }
328 :
329 : /// An alternative representation of `pageserver::tenant::LocationConf`,
330 : /// for use in external-facing APIs.
331 9348 : #[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
332 : pub struct LocationConfig {
333 : pub mode: LocationConfigMode,
334 : /// If attaching, in what generation?
335 : #[serde(default)]
336 : pub generation: Option<u32>,
337 :
338 : // If requesting mode `Secondary`, configuration for that.
339 : #[serde(default)]
340 : pub secondary_conf: Option<LocationConfigSecondary>,
341 :
342 : // Shard parameters: if shard_count is nonzero, then other shard_* fields
343 : // must be set accurately.
344 : #[serde(default)]
345 : pub shard_number: u8,
346 : #[serde(default)]
347 : pub shard_count: u8,
348 : #[serde(default)]
349 : pub shard_stripe_size: u32,
350 :
351 : // This configuration only affects attached mode, but should be provided irrespective
352 : // of the mode, as a secondary location might transition on startup if the response
353 : // to the `/re-attach` control plane API requests it.
354 : pub tenant_conf: TenantConfig,
355 : }
356 :
357 18 : #[derive(Serialize, Deserialize)]
358 : pub struct LocationConfigListResponse {
359 : pub tenant_shards: Vec<(TenantShardId, Option<LocationConfig>)>,
360 : }
361 :
362 45 : #[derive(Serialize, Deserialize)]
363 : #[serde(transparent)]
364 : pub struct TenantCreateResponse(pub TenantId);
365 :
366 630 : #[derive(Serialize)]
367 : pub struct StatusResponse {
368 : pub id: NodeId,
369 : }
370 :
371 7041 : #[derive(Serialize, Deserialize, Debug)]
372 : #[serde(deny_unknown_fields)]
373 : pub struct TenantLocationConfigRequest {
374 : pub tenant_id: TenantShardId,
375 : #[serde(flatten)]
376 : pub config: LocationConfig, // as we have a flattened field, we should reject all unknown fields in it
377 : }
378 :
379 596 : #[derive(Serialize, Deserialize, Debug)]
380 : #[serde(deny_unknown_fields)]
381 : pub struct TenantShardLocation {
382 : pub shard_id: TenantShardId,
383 : pub node_id: NodeId,
384 : }
385 :
386 630 : #[derive(Serialize, Deserialize, Debug)]
387 : #[serde(deny_unknown_fields)]
388 : pub struct TenantLocationConfigResponse {
389 : pub shards: Vec<TenantShardLocation>,
390 : }
391 :
392 457 : #[derive(Serialize, Deserialize, Debug)]
393 : #[serde(deny_unknown_fields)]
394 : pub struct TenantConfigRequest {
395 : pub tenant_id: TenantId,
396 : #[serde(flatten)]
397 : pub config: TenantConfig, // as we have a flattened field, we should reject all unknown fields in it
398 : }
399 :
400 : impl std::ops::Deref for TenantConfigRequest {
401 : type Target = TenantConfig;
402 :
403 0 : fn deref(&self) -> &Self::Target {
404 0 : &self.config
405 0 : }
406 : }
407 :
408 : impl TenantConfigRequest {
409 0 : pub fn new(tenant_id: TenantId) -> TenantConfigRequest {
410 0 : let config = TenantConfig::default();
411 0 : TenantConfigRequest { tenant_id, config }
412 0 : }
413 : }
414 :
415 202 : #[derive(Debug, Deserialize)]
416 : pub struct TenantAttachRequest {
417 : #[serde(default)]
418 : pub config: TenantAttachConfig,
419 : #[serde(default)]
420 : pub generation: Option<u32>,
421 : }
422 :
423 : /// Newtype to enforce deny_unknown_fields on TenantConfig for
424 : /// its usage inside `TenantAttachRequest`.
425 75 : #[derive(Debug, Serialize, Deserialize, Default)]
426 : #[serde(deny_unknown_fields)]
427 : pub struct TenantAttachConfig {
428 : #[serde(flatten)]
429 : allowing_unknown_fields: TenantConfig,
430 : }
431 :
432 : impl std::ops::Deref for TenantAttachConfig {
433 : type Target = TenantConfig;
434 :
435 38 : fn deref(&self) -> &Self::Target {
436 38 : &self.allowing_unknown_fields
437 38 : }
438 : }
439 :
440 : /// See [`TenantState::attachment_status`] and the OpenAPI docs for context.
441 767 : #[derive(Serialize, Deserialize, Clone)]
442 : #[serde(tag = "slug", content = "data", rename_all = "snake_case")]
443 : pub enum TenantAttachmentStatus {
444 : Maybe,
445 : Attached,
446 : Failed { reason: String },
447 : }
448 :
449 664 : #[derive(Serialize, Deserialize, Clone)]
450 : pub struct TenantInfo {
451 : pub id: TenantShardId,
452 : // NB: intentionally not part of OpenAPI, we don't want to commit to a specific set of TenantState's
453 : pub state: TenantState,
454 : /// Sum of the size of all layer files.
455 : /// If a layer is present in both local FS and S3, it counts only once.
456 : pub current_physical_size: Option<u64>, // physical size is only included in `tenant_status` endpoint
457 : pub attachment_status: TenantAttachmentStatus,
458 : #[serde(skip_serializing_if = "Option::is_none")]
459 : pub generation: Option<u32>,
460 : }
461 :
462 479 : #[derive(Serialize, Deserialize, Clone)]
463 : pub struct TenantDetails {
464 : #[serde(flatten)]
465 : pub tenant_info: TenantInfo,
466 :
467 : pub walredo: Option<WalRedoManagerStatus>,
468 :
469 : pub timelines: Vec<TimelineId>,
470 : }
471 :
472 : /// This represents the output of the "timeline_detail" and "timeline_list" API calls.
473 74430 : #[derive(Debug, Serialize, Deserialize, Clone)]
474 : pub struct TimelineInfo {
475 : pub tenant_id: TenantShardId,
476 : pub timeline_id: TimelineId,
477 :
478 : pub ancestor_timeline_id: Option<TimelineId>,
479 : pub ancestor_lsn: Option<Lsn>,
480 : pub last_record_lsn: Lsn,
481 : pub prev_record_lsn: Option<Lsn>,
482 : pub latest_gc_cutoff_lsn: Lsn,
483 : pub disk_consistent_lsn: Lsn,
484 :
485 : /// The LSN that we have succesfully uploaded to remote storage
486 : pub remote_consistent_lsn: Lsn,
487 :
488 : /// The LSN that we are advertizing to safekeepers
489 : pub remote_consistent_lsn_visible: Lsn,
490 :
491 : /// The LSN from the start of the root timeline (never changes)
492 : pub initdb_lsn: Lsn,
493 :
494 : pub current_logical_size: u64,
495 : pub current_logical_size_is_accurate: bool,
496 :
497 : /// Sum of the size of all layer files.
498 : /// If a layer is present in both local FS and S3, it counts only once.
499 : pub current_physical_size: Option<u64>, // is None when timeline is Unloaded
500 : pub current_logical_size_non_incremental: Option<u64>,
501 :
502 : pub timeline_dir_layer_file_size_sum: Option<u64>,
503 :
504 : pub wal_source_connstr: Option<String>,
505 : pub last_received_msg_lsn: Option<Lsn>,
506 : /// the timestamp (in microseconds) of the last received message
507 : pub last_received_msg_ts: Option<u128>,
508 : pub pg_version: u32,
509 :
510 : pub state: TimelineState,
511 :
512 : pub walreceiver_status: String,
513 : }
514 :
515 116 : #[derive(Debug, Clone, Serialize)]
516 : pub struct LayerMapInfo {
517 : pub in_memory_layers: Vec<InMemoryLayerInfo>,
518 : pub historic_layers: Vec<HistoricLayerInfo>,
519 : }
520 :
521 33768466 : #[derive(Debug, Hash, PartialEq, Eq, Clone, Copy, Serialize, Deserialize, enum_map::Enum)]
522 : #[repr(usize)]
523 : pub enum LayerAccessKind {
524 : GetValueReconstructData,
525 : Iter,
526 : KeyIter,
527 : Dump,
528 : }
529 :
530 20189 : #[derive(Debug, Clone, Serialize, Deserialize)]
531 : pub struct LayerAccessStatFullDetails {
532 : pub when_millis_since_epoch: u64,
533 : pub task_kind: &'static str,
534 : pub access_kind: LayerAccessKind,
535 : }
536 :
537 : /// An event that impacts the layer's residence status.
538 : #[serde_as]
539 7376 : #[derive(Debug, Clone, Serialize, Deserialize)]
540 : pub struct LayerResidenceEvent {
541 : /// The time when the event occurred.
542 : /// NB: this timestamp is captured while the residence status changes.
543 : /// So, it might be behind/ahead of the actual residence change by a short amount of time.
544 : ///
545 : #[serde(rename = "timestamp_millis_since_epoch")]
546 : #[serde_as(as = "serde_with::TimestampMilliSeconds")]
547 : pub timestamp: SystemTime,
548 : /// The new residence status of the layer.
549 : pub status: LayerResidenceStatus,
550 : /// The reason why we had to record this event.
551 : pub reason: LayerResidenceEventReason,
552 : }
553 :
554 : /// The reason for recording a given [`LayerResidenceEvent`].
555 7376 : #[derive(Debug, Clone, Copy, Serialize, Deserialize)]
556 : pub enum LayerResidenceEventReason {
557 : /// The layer map is being populated, e.g. during timeline load or attach.
558 : /// This includes [`RemoteLayer`] objects created in [`reconcile_with_remote`].
559 : /// We need to record such events because there is no persistent storage for the events.
560 : ///
561 : // https://github.com/rust-lang/rust/issues/74481
562 : /// [`RemoteLayer`]: ../../tenant/storage_layer/struct.RemoteLayer.html
563 : /// [`reconcile_with_remote`]: ../../tenant/struct.Timeline.html#method.reconcile_with_remote
564 : LayerLoad,
565 : /// We just created the layer (e.g., freeze_and_flush or compaction).
566 : /// Such layers are always [`LayerResidenceStatus::Resident`].
567 : LayerCreate,
568 : /// We on-demand downloaded or evicted the given layer.
569 : ResidenceChange,
570 : }
571 :
572 : /// The residence status of the layer, after the given [`LayerResidenceEvent`].
573 7376 : #[derive(Debug, Clone, Copy, Serialize, Deserialize)]
574 : pub enum LayerResidenceStatus {
575 : /// Residence status for a layer file that exists locally.
576 : /// It may also exist on the remote, we don't care here.
577 : Resident,
578 : /// Residence status for a layer file that only exists on the remote.
579 : Evicted,
580 : }
581 :
582 : impl LayerResidenceEvent {
583 175466 : pub fn new(status: LayerResidenceStatus, reason: LayerResidenceEventReason) -> Self {
584 175466 : Self {
585 175466 : status,
586 175466 : reason,
587 175466 : timestamp: SystemTime::now(),
588 175466 : }
589 175466 : }
590 : }
591 :
592 3151 : #[derive(Debug, Clone, Serialize)]
593 : pub struct LayerAccessStats {
594 : pub access_count_by_access_kind: HashMap<LayerAccessKind, u64>,
595 : pub task_kind_access_flag: Vec<&'static str>,
596 : pub first: Option<LayerAccessStatFullDetails>,
597 : pub accesses_history: HistoryBufferWithDropCounter<LayerAccessStatFullDetails, 16>,
598 : pub residence_events_history: HistoryBufferWithDropCounter<LayerResidenceEvent, 16>,
599 : }
600 :
601 27 : #[derive(Debug, Clone, Serialize)]
602 : #[serde(tag = "kind")]
603 : pub enum InMemoryLayerInfo {
604 : Open { lsn_start: Lsn },
605 : Frozen { lsn_start: Lsn, lsn_end: Lsn },
606 : }
607 :
608 3151 : #[derive(Debug, Clone, Serialize)]
609 : #[serde(tag = "kind")]
610 : pub enum HistoricLayerInfo {
611 : Delta {
612 : layer_file_name: String,
613 : layer_file_size: u64,
614 :
615 : lsn_start: Lsn,
616 : lsn_end: Lsn,
617 : remote: bool,
618 : access_stats: LayerAccessStats,
619 : },
620 : Image {
621 : layer_file_name: String,
622 : layer_file_size: u64,
623 :
624 : lsn_start: Lsn,
625 : remote: bool,
626 : access_stats: LayerAccessStats,
627 : },
628 : }
629 :
630 6 : #[derive(Debug, Serialize, Deserialize)]
631 : pub struct DownloadRemoteLayersTaskSpawnRequest {
632 : pub max_concurrent_downloads: NonZeroUsize,
633 : }
634 :
635 51 : #[derive(Debug, Serialize, Deserialize, Clone)]
636 : pub struct DownloadRemoteLayersTaskInfo {
637 : pub task_id: String,
638 : pub state: DownloadRemoteLayersTaskState,
639 : pub total_layer_count: u64, // stable once `completed`
640 : pub successful_download_count: u64, // stable once `completed`
641 : pub failed_download_count: u64, // stable once `completed`
642 : }
643 :
644 51 : #[derive(Debug, Serialize, Deserialize, Clone)]
645 : pub enum DownloadRemoteLayersTaskState {
646 : Running,
647 : Completed,
648 : ShutDown,
649 : }
650 :
651 1122 : #[derive(Debug, Serialize, Deserialize)]
652 : pub struct TimelineGcRequest {
653 : pub gc_horizon: Option<u64>,
654 : }
655 :
656 479 : #[derive(Debug, Clone, Serialize, Deserialize)]
657 : pub struct WalRedoManagerStatus {
658 : pub last_redo_at: Option<chrono::DateTime<chrono::Utc>>,
659 : pub pid: Option<u32>,
660 : }
661 :
662 : pub mod virtual_file {
663 : #[derive(
664 : Copy,
665 0 : Clone,
666 4 : PartialEq,
667 : Eq,
668 0 : Hash,
669 1241 : strum_macros::EnumString,
670 625 : strum_macros::Display,
671 0 : serde_with::DeserializeFromStr,
672 0 : serde_with::SerializeDisplay,
673 0 : Debug,
674 : )]
675 : #[strum(serialize_all = "kebab-case")]
676 : pub enum IoEngineKind {
677 : StdFs,
678 : #[cfg(target_os = "linux")]
679 : TokioEpollUring,
680 : }
681 : }
682 :
683 : // Wrapped in libpq CopyData
684 8 : #[derive(PartialEq, Eq, Debug)]
685 : pub enum PagestreamFeMessage {
686 : Exists(PagestreamExistsRequest),
687 : Nblocks(PagestreamNblocksRequest),
688 : GetPage(PagestreamGetPageRequest),
689 : DbSize(PagestreamDbSizeRequest),
690 : GetSlruSegment(PagestreamGetSlruSegmentRequest),
691 : }
692 :
693 : // Wrapped in libpq CopyData
694 0 : #[derive(strum_macros::EnumProperty)]
695 : pub enum PagestreamBeMessage {
696 : Exists(PagestreamExistsResponse),
697 : Nblocks(PagestreamNblocksResponse),
698 : GetPage(PagestreamGetPageResponse),
699 : Error(PagestreamErrorResponse),
700 : DbSize(PagestreamDbSizeResponse),
701 : GetSlruSegment(PagestreamGetSlruSegmentResponse),
702 : }
703 :
704 : // Keep in sync with `pagestore_client.h`
705 : #[repr(u8)]
706 : enum PagestreamBeMessageTag {
707 : Exists = 100,
708 : Nblocks = 101,
709 : GetPage = 102,
710 : Error = 103,
711 : DbSize = 104,
712 : GetSlruSegment = 105,
713 : }
714 : impl TryFrom<u8> for PagestreamBeMessageTag {
715 : type Error = u8;
716 0 : fn try_from(value: u8) -> Result<Self, u8> {
717 0 : match value {
718 0 : 100 => Ok(PagestreamBeMessageTag::Exists),
719 0 : 101 => Ok(PagestreamBeMessageTag::Nblocks),
720 0 : 102 => Ok(PagestreamBeMessageTag::GetPage),
721 0 : 103 => Ok(PagestreamBeMessageTag::Error),
722 0 : 104 => Ok(PagestreamBeMessageTag::DbSize),
723 0 : 105 => Ok(PagestreamBeMessageTag::GetSlruSegment),
724 0 : _ => Err(value),
725 : }
726 0 : }
727 : }
728 :
729 2 : #[derive(Debug, PartialEq, Eq)]
730 : pub struct PagestreamExistsRequest {
731 : pub latest: bool,
732 : pub lsn: Lsn,
733 : pub rel: RelTag,
734 : }
735 :
736 2 : #[derive(Debug, PartialEq, Eq)]
737 : pub struct PagestreamNblocksRequest {
738 : pub latest: bool,
739 : pub lsn: Lsn,
740 : pub rel: RelTag,
741 : }
742 :
743 2 : #[derive(Debug, PartialEq, Eq)]
744 : pub struct PagestreamGetPageRequest {
745 : pub latest: bool,
746 : pub lsn: Lsn,
747 : pub rel: RelTag,
748 : pub blkno: u32,
749 : }
750 :
751 2 : #[derive(Debug, PartialEq, Eq)]
752 : pub struct PagestreamDbSizeRequest {
753 : pub latest: bool,
754 : pub lsn: Lsn,
755 : pub dbnode: u32,
756 : }
757 :
758 0 : #[derive(Debug, PartialEq, Eq)]
759 : pub struct PagestreamGetSlruSegmentRequest {
760 : pub latest: bool,
761 : pub lsn: Lsn,
762 : pub kind: u8,
763 : pub segno: u32,
764 : }
765 :
766 0 : #[derive(Debug)]
767 : pub struct PagestreamExistsResponse {
768 : pub exists: bool,
769 : }
770 :
771 0 : #[derive(Debug)]
772 : pub struct PagestreamNblocksResponse {
773 : pub n_blocks: u32,
774 : }
775 :
776 0 : #[derive(Debug)]
777 : pub struct PagestreamGetPageResponse {
778 : pub page: Bytes,
779 : }
780 :
781 0 : #[derive(Debug)]
782 : pub struct PagestreamGetSlruSegmentResponse {
783 : pub segment: Bytes,
784 : }
785 :
786 0 : #[derive(Debug)]
787 : pub struct PagestreamErrorResponse {
788 : pub message: String,
789 : }
790 :
791 0 : #[derive(Debug)]
792 : pub struct PagestreamDbSizeResponse {
793 : pub db_size: i64,
794 : }
795 :
796 : // This is a cut-down version of TenantHistorySize from the pageserver crate, omitting fields
797 : // that require pageserver-internal types. It is sufficient to get the total size.
798 0 : #[derive(Serialize, Deserialize, Debug)]
799 : pub struct TenantHistorySize {
800 : pub id: TenantId,
801 : /// Size is a mixture of WAL and logical size, so the unit is bytes.
802 : ///
803 : /// Will be none if `?inputs_only=true` was given.
804 : pub size: Option<u64>,
805 : }
806 :
807 : impl PagestreamFeMessage {
808 8 : pub fn serialize(&self) -> Bytes {
809 8 : let mut bytes = BytesMut::new();
810 8 :
811 8 : match self {
812 2 : Self::Exists(req) => {
813 2 : bytes.put_u8(0);
814 2 : bytes.put_u8(u8::from(req.latest));
815 2 : bytes.put_u64(req.lsn.0);
816 2 : bytes.put_u32(req.rel.spcnode);
817 2 : bytes.put_u32(req.rel.dbnode);
818 2 : bytes.put_u32(req.rel.relnode);
819 2 : bytes.put_u8(req.rel.forknum);
820 2 : }
821 :
822 2 : Self::Nblocks(req) => {
823 2 : bytes.put_u8(1);
824 2 : bytes.put_u8(u8::from(req.latest));
825 2 : bytes.put_u64(req.lsn.0);
826 2 : bytes.put_u32(req.rel.spcnode);
827 2 : bytes.put_u32(req.rel.dbnode);
828 2 : bytes.put_u32(req.rel.relnode);
829 2 : bytes.put_u8(req.rel.forknum);
830 2 : }
831 :
832 2 : Self::GetPage(req) => {
833 2 : bytes.put_u8(2);
834 2 : bytes.put_u8(u8::from(req.latest));
835 2 : bytes.put_u64(req.lsn.0);
836 2 : bytes.put_u32(req.rel.spcnode);
837 2 : bytes.put_u32(req.rel.dbnode);
838 2 : bytes.put_u32(req.rel.relnode);
839 2 : bytes.put_u8(req.rel.forknum);
840 2 : bytes.put_u32(req.blkno);
841 2 : }
842 :
843 2 : Self::DbSize(req) => {
844 2 : bytes.put_u8(3);
845 2 : bytes.put_u8(u8::from(req.latest));
846 2 : bytes.put_u64(req.lsn.0);
847 2 : bytes.put_u32(req.dbnode);
848 2 : }
849 :
850 0 : Self::GetSlruSegment(req) => {
851 0 : bytes.put_u8(4);
852 0 : bytes.put_u8(u8::from(req.latest));
853 0 : bytes.put_u64(req.lsn.0);
854 0 : bytes.put_u8(req.kind);
855 0 : bytes.put_u32(req.segno);
856 0 : }
857 : }
858 :
859 8 : bytes.into()
860 8 : }
861 :
862 4509145 : pub fn parse<R: std::io::Read>(body: &mut R) -> anyhow::Result<PagestreamFeMessage> {
863 : // TODO these gets can fail
864 :
865 : // these correspond to the NeonMessageTag enum in pagestore_client.h
866 : //
867 : // TODO: consider using protobuf or serde bincode for less error prone
868 : // serialization.
869 4509145 : let msg_tag = body.read_u8()?;
870 4509145 : match msg_tag {
871 : 0 => Ok(PagestreamFeMessage::Exists(PagestreamExistsRequest {
872 56705 : latest: body.read_u8()? != 0,
873 56705 : lsn: Lsn::from(body.read_u64::<BigEndian>()?),
874 : rel: RelTag {
875 56705 : spcnode: body.read_u32::<BigEndian>()?,
876 56705 : dbnode: body.read_u32::<BigEndian>()?,
877 56705 : relnode: body.read_u32::<BigEndian>()?,
878 56705 : forknum: body.read_u8()?,
879 : },
880 : })),
881 : 1 => Ok(PagestreamFeMessage::Nblocks(PagestreamNblocksRequest {
882 20225 : latest: body.read_u8()? != 0,
883 20225 : lsn: Lsn::from(body.read_u64::<BigEndian>()?),
884 : rel: RelTag {
885 20225 : spcnode: body.read_u32::<BigEndian>()?,
886 20225 : dbnode: body.read_u32::<BigEndian>()?,
887 20225 : relnode: body.read_u32::<BigEndian>()?,
888 20225 : forknum: body.read_u8()?,
889 : },
890 : })),
891 : 2 => Ok(PagestreamFeMessage::GetPage(PagestreamGetPageRequest {
892 4432208 : latest: body.read_u8()? != 0,
893 4432208 : lsn: Lsn::from(body.read_u64::<BigEndian>()?),
894 : rel: RelTag {
895 4432208 : spcnode: body.read_u32::<BigEndian>()?,
896 4432208 : dbnode: body.read_u32::<BigEndian>()?,
897 4432208 : relnode: body.read_u32::<BigEndian>()?,
898 4432208 : forknum: body.read_u8()?,
899 : },
900 4432208 : blkno: body.read_u32::<BigEndian>()?,
901 : })),
902 : 3 => Ok(PagestreamFeMessage::DbSize(PagestreamDbSizeRequest {
903 7 : latest: body.read_u8()? != 0,
904 7 : lsn: Lsn::from(body.read_u64::<BigEndian>()?),
905 7 : dbnode: body.read_u32::<BigEndian>()?,
906 : })),
907 : 4 => Ok(PagestreamFeMessage::GetSlruSegment(
908 : PagestreamGetSlruSegmentRequest {
909 0 : latest: body.read_u8()? != 0,
910 0 : lsn: Lsn::from(body.read_u64::<BigEndian>()?),
911 0 : kind: body.read_u8()?,
912 0 : segno: body.read_u32::<BigEndian>()?,
913 : },
914 : )),
915 0 : _ => bail!("unknown smgr message tag: {:?}", msg_tag),
916 : }
917 4509145 : }
918 : }
919 :
920 : impl PagestreamBeMessage {
921 4574751 : pub fn serialize(&self) -> Bytes {
922 4574751 : let mut bytes = BytesMut::new();
923 4574751 :
924 4574751 : use PagestreamBeMessageTag as Tag;
925 4574751 : match self {
926 56809 : Self::Exists(resp) => {
927 56809 : bytes.put_u8(Tag::Exists as u8);
928 56809 : bytes.put_u8(resp.exists as u8);
929 56809 : }
930 :
931 20589 : Self::Nblocks(resp) => {
932 20589 : bytes.put_u8(Tag::Nblocks as u8);
933 20589 : bytes.put_u32(resp.n_blocks);
934 20589 : }
935 :
936 4497347 : Self::GetPage(resp) => {
937 4497347 : bytes.put_u8(Tag::GetPage as u8);
938 4497347 : bytes.put(&resp.page[..]);
939 4497347 : }
940 :
941 1 : Self::Error(resp) => {
942 1 : bytes.put_u8(Tag::Error as u8);
943 1 : bytes.put(resp.message.as_bytes());
944 1 : bytes.put_u8(0); // null terminator
945 1 : }
946 5 : Self::DbSize(resp) => {
947 5 : bytes.put_u8(Tag::DbSize as u8);
948 5 : bytes.put_i64(resp.db_size);
949 5 : }
950 :
951 0 : Self::GetSlruSegment(resp) => {
952 0 : bytes.put_u8(Tag::GetSlruSegment as u8);
953 0 : bytes.put_u32((resp.segment.len() / BLCKSZ as usize) as u32);
954 0 : bytes.put(&resp.segment[..]);
955 0 : }
956 : }
957 :
958 4574751 : bytes.into()
959 4574751 : }
960 :
961 0 : pub fn deserialize(buf: Bytes) -> anyhow::Result<Self> {
962 0 : let mut buf = buf.reader();
963 0 : let msg_tag = buf.read_u8()?;
964 :
965 : use PagestreamBeMessageTag as Tag;
966 0 : let ok =
967 0 : match Tag::try_from(msg_tag).map_err(|tag: u8| anyhow::anyhow!("invalid tag {tag}"))? {
968 : Tag::Exists => {
969 0 : let exists = buf.read_u8()?;
970 0 : Self::Exists(PagestreamExistsResponse {
971 0 : exists: exists != 0,
972 0 : })
973 : }
974 : Tag::Nblocks => {
975 0 : let n_blocks = buf.read_u32::<BigEndian>()?;
976 0 : Self::Nblocks(PagestreamNblocksResponse { n_blocks })
977 : }
978 : Tag::GetPage => {
979 0 : let mut page = vec![0; 8192]; // TODO: use MaybeUninit
980 0 : buf.read_exact(&mut page)?;
981 0 : PagestreamBeMessage::GetPage(PagestreamGetPageResponse { page: page.into() })
982 : }
983 : Tag::Error => {
984 0 : let mut msg = Vec::new();
985 0 : buf.read_until(0, &mut msg)?;
986 0 : let cstring = std::ffi::CString::from_vec_with_nul(msg)?;
987 0 : let rust_str = cstring.to_str()?;
988 0 : PagestreamBeMessage::Error(PagestreamErrorResponse {
989 0 : message: rust_str.to_owned(),
990 0 : })
991 : }
992 : Tag::DbSize => {
993 0 : let db_size = buf.read_i64::<BigEndian>()?;
994 0 : Self::DbSize(PagestreamDbSizeResponse { db_size })
995 : }
996 : Tag::GetSlruSegment => {
997 0 : let n_blocks = buf.read_u32::<BigEndian>()?;
998 0 : let mut segment = vec![0; n_blocks as usize * BLCKSZ as usize];
999 0 : buf.read_exact(&mut segment)?;
1000 0 : Self::GetSlruSegment(PagestreamGetSlruSegmentResponse {
1001 0 : segment: segment.into(),
1002 0 : })
1003 : }
1004 : };
1005 0 : let remaining = buf.into_inner();
1006 0 : if !remaining.is_empty() {
1007 0 : anyhow::bail!(
1008 0 : "remaining bytes in msg with tag={msg_tag}: {}",
1009 0 : remaining.len()
1010 0 : );
1011 0 : }
1012 0 : Ok(ok)
1013 0 : }
1014 :
1015 0 : pub fn kind(&self) -> &'static str {
1016 0 : match self {
1017 0 : Self::Exists(_) => "Exists",
1018 0 : Self::Nblocks(_) => "Nblocks",
1019 0 : Self::GetPage(_) => "GetPage",
1020 0 : Self::Error(_) => "Error",
1021 0 : Self::DbSize(_) => "DbSize",
1022 0 : Self::GetSlruSegment(_) => "GetSlruSegment",
1023 : }
1024 0 : }
1025 : }
1026 :
1027 : #[cfg(test)]
1028 : mod tests {
1029 : use bytes::Buf;
1030 : use serde_json::json;
1031 :
1032 : use super::*;
1033 :
1034 2 : #[test]
1035 2 : fn test_pagestream() {
1036 2 : // Test serialization/deserialization of PagestreamFeMessage
1037 2 : let messages = vec![
1038 2 : PagestreamFeMessage::Exists(PagestreamExistsRequest {
1039 2 : latest: true,
1040 2 : lsn: Lsn(4),
1041 2 : rel: RelTag {
1042 2 : forknum: 1,
1043 2 : spcnode: 2,
1044 2 : dbnode: 3,
1045 2 : relnode: 4,
1046 2 : },
1047 2 : }),
1048 2 : PagestreamFeMessage::Nblocks(PagestreamNblocksRequest {
1049 2 : latest: false,
1050 2 : lsn: Lsn(4),
1051 2 : rel: RelTag {
1052 2 : forknum: 1,
1053 2 : spcnode: 2,
1054 2 : dbnode: 3,
1055 2 : relnode: 4,
1056 2 : },
1057 2 : }),
1058 2 : PagestreamFeMessage::GetPage(PagestreamGetPageRequest {
1059 2 : latest: true,
1060 2 : lsn: Lsn(4),
1061 2 : rel: RelTag {
1062 2 : forknum: 1,
1063 2 : spcnode: 2,
1064 2 : dbnode: 3,
1065 2 : relnode: 4,
1066 2 : },
1067 2 : blkno: 7,
1068 2 : }),
1069 2 : PagestreamFeMessage::DbSize(PagestreamDbSizeRequest {
1070 2 : latest: true,
1071 2 : lsn: Lsn(4),
1072 2 : dbnode: 7,
1073 2 : }),
1074 2 : ];
1075 10 : for msg in messages {
1076 8 : let bytes = msg.serialize();
1077 8 : let reconstructed = PagestreamFeMessage::parse(&mut bytes.reader()).unwrap();
1078 8 : assert!(msg == reconstructed);
1079 : }
1080 2 : }
1081 :
1082 2 : #[test]
1083 2 : fn test_tenantinfo_serde() {
1084 2 : // Test serialization/deserialization of TenantInfo
1085 2 : let original_active = TenantInfo {
1086 2 : id: TenantShardId::unsharded(TenantId::generate()),
1087 2 : state: TenantState::Active,
1088 2 : current_physical_size: Some(42),
1089 2 : attachment_status: TenantAttachmentStatus::Attached,
1090 2 : generation: None,
1091 2 : };
1092 2 : let expected_active = json!({
1093 2 : "id": original_active.id.to_string(),
1094 2 : "state": {
1095 2 : "slug": "Active",
1096 2 : },
1097 2 : "current_physical_size": 42,
1098 2 : "attachment_status": {
1099 2 : "slug":"attached",
1100 2 : }
1101 2 : });
1102 2 :
1103 2 : let original_broken = TenantInfo {
1104 2 : id: TenantShardId::unsharded(TenantId::generate()),
1105 2 : state: TenantState::Broken {
1106 2 : reason: "reason".into(),
1107 2 : backtrace: "backtrace info".into(),
1108 2 : },
1109 2 : current_physical_size: Some(42),
1110 2 : attachment_status: TenantAttachmentStatus::Attached,
1111 2 : generation: None,
1112 2 : };
1113 2 : let expected_broken = json!({
1114 2 : "id": original_broken.id.to_string(),
1115 2 : "state": {
1116 2 : "slug": "Broken",
1117 2 : "data": {
1118 2 : "backtrace": "backtrace info",
1119 2 : "reason": "reason",
1120 2 : }
1121 2 : },
1122 2 : "current_physical_size": 42,
1123 2 : "attachment_status": {
1124 2 : "slug":"attached",
1125 2 : }
1126 2 : });
1127 2 :
1128 2 : assert_eq!(
1129 2 : serde_json::to_value(&original_active).unwrap(),
1130 2 : expected_active
1131 2 : );
1132 :
1133 2 : assert_eq!(
1134 2 : serde_json::to_value(&original_broken).unwrap(),
1135 2 : expected_broken
1136 2 : );
1137 2 : assert!(format!("{:?}", &original_broken.state).contains("reason"));
1138 2 : assert!(format!("{:?}", &original_broken.state).contains("backtrace info"));
1139 2 : }
1140 :
1141 2 : #[test]
1142 2 : fn test_reject_unknown_field() {
1143 2 : let id = TenantId::generate();
1144 2 : let create_request = json!({
1145 2 : "new_tenant_id": id.to_string(),
1146 2 : "unknown_field": "unknown_value".to_string(),
1147 2 : });
1148 2 : let err = serde_json::from_value::<TenantCreateRequest>(create_request).unwrap_err();
1149 2 : assert!(
1150 2 : err.to_string().contains("unknown field `unknown_field`"),
1151 0 : "expect unknown field `unknown_field` error, got: {}",
1152 : err
1153 : );
1154 :
1155 2 : let id = TenantId::generate();
1156 2 : let config_request = json!({
1157 2 : "tenant_id": id.to_string(),
1158 2 : "unknown_field": "unknown_value".to_string(),
1159 2 : });
1160 2 : let err = serde_json::from_value::<TenantConfigRequest>(config_request).unwrap_err();
1161 2 : assert!(
1162 2 : err.to_string().contains("unknown field `unknown_field`"),
1163 0 : "expect unknown field `unknown_field` error, got: {}",
1164 : err
1165 : );
1166 :
1167 2 : let attach_request = json!({
1168 2 : "config": {
1169 2 : "unknown_field": "unknown_value".to_string(),
1170 2 : },
1171 2 : });
1172 2 : let err = serde_json::from_value::<TenantAttachRequest>(attach_request).unwrap_err();
1173 2 : assert!(
1174 2 : err.to_string().contains("unknown field `unknown_field`"),
1175 0 : "expect unknown field `unknown_field` error, got: {}",
1176 : err
1177 : );
1178 2 : }
1179 :
1180 2 : #[test]
1181 2 : fn tenantstatus_activating_serde() {
1182 2 : let states = [
1183 2 : TenantState::Activating(ActivatingFrom::Loading),
1184 2 : TenantState::Activating(ActivatingFrom::Attaching),
1185 2 : ];
1186 2 : let expected = "[{\"slug\":\"Activating\",\"data\":\"Loading\"},{\"slug\":\"Activating\",\"data\":\"Attaching\"}]";
1187 2 :
1188 2 : let actual = serde_json::to_string(&states).unwrap();
1189 2 :
1190 2 : assert_eq!(actual, expected);
1191 :
1192 2 : let parsed = serde_json::from_str::<Vec<TenantState>>(&actual).unwrap();
1193 2 :
1194 2 : assert_eq!(states.as_slice(), &parsed);
1195 2 : }
1196 :
1197 2 : #[test]
1198 2 : fn tenantstatus_activating_strum() {
1199 2 : // tests added, because we use these for metrics
1200 2 : let examples = [
1201 2 : (line!(), TenantState::Loading, "Loading"),
1202 2 : (line!(), TenantState::Attaching, "Attaching"),
1203 2 : (
1204 2 : line!(),
1205 2 : TenantState::Activating(ActivatingFrom::Loading),
1206 2 : "Activating",
1207 2 : ),
1208 2 : (
1209 2 : line!(),
1210 2 : TenantState::Activating(ActivatingFrom::Attaching),
1211 2 : "Activating",
1212 2 : ),
1213 2 : (line!(), TenantState::Active, "Active"),
1214 2 : (
1215 2 : line!(),
1216 2 : TenantState::Stopping {
1217 2 : progress: utils::completion::Barrier::default(),
1218 2 : },
1219 2 : "Stopping",
1220 2 : ),
1221 2 : (
1222 2 : line!(),
1223 2 : TenantState::Broken {
1224 2 : reason: "Example".into(),
1225 2 : backtrace: "Looooong backtrace".into(),
1226 2 : },
1227 2 : "Broken",
1228 2 : ),
1229 2 : ];
1230 :
1231 16 : for (line, rendered, expected) in examples {
1232 14 : let actual: &'static str = rendered.into();
1233 14 : assert_eq!(actual, expected, "example on {line}");
1234 : }
1235 2 : }
1236 : }
|