LCOV - code coverage report
Current view: top level - pageserver/src/tenant - config.rs (source / functions) Coverage Total Hit
Test: 49aa928ec5b4b510172d8b5c6d154da28e70a46c.info Lines: 27.5 % 244 67
Test Date: 2024-11-13 18:23:39 Functions: 11.5 % 130 15

            Line data    Source code
       1              : //! Functions for handling per-tenant configuration options
       2              : //!
       3              : //! If tenant is created with --config option,
       4              : //! the tenant-specific config will be stored in tenant's directory.
       5              : //! Otherwise, global pageserver's config is used.
       6              : //!
       7              : //! If the tenant config file is corrupted, the tenant will be disabled.
       8              : //! We cannot use global or default config instead, because wrong settings
       9              : //! may lead to a data loss.
      10              : //!
      11              : pub(crate) use pageserver_api::config::TenantConfigToml as TenantConf;
      12              : use pageserver_api::models::CompactionAlgorithmSettings;
      13              : use pageserver_api::models::EvictionPolicy;
      14              : use pageserver_api::models::{self, ThrottleConfig};
      15              : use pageserver_api::shard::{ShardCount, ShardIdentity, ShardNumber, ShardStripeSize};
      16              : use serde::de::IntoDeserializer;
      17              : use serde::{Deserialize, Serialize};
      18              : use serde_json::Value;
      19              : use std::num::NonZeroU64;
      20              : use std::time::Duration;
      21              : use utils::generation::Generation;
      22              : 
      23            0 : #[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
      24              : pub(crate) enum AttachmentMode {
      25              :     /// Our generation is current as far as we know, and as far as we know we are the only attached
      26              :     /// pageserver.  This is the "normal" attachment mode.
      27              :     Single,
      28              :     /// Our generation number is current as far as we know, but we are advised that another
      29              :     /// pageserver is still attached, and therefore to avoid executing deletions.   This is
      30              :     /// the attachment mode of a pagesever that is the destination of a migration.
      31              :     Multi,
      32              :     /// Our generation number is superseded, or about to be superseded.  We are advised
      33              :     /// to avoid remote storage writes if possible, and to avoid sending billing data.  This
      34              :     /// is the attachment mode of a pageserver that is the origin of a migration.
      35              :     Stale,
      36              : }
      37              : 
      38            0 : #[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
      39              : pub(crate) struct AttachedLocationConfig {
      40              :     pub(crate) generation: Generation,
      41              :     pub(crate) attach_mode: AttachmentMode,
      42              :     // TODO: add a flag to override AttachmentMode's policies under
      43              :     // disk pressure (i.e. unblock uploads under disk pressure in Stale
      44              :     // state, unblock deletions after timeout in Multi state)
      45              : }
      46              : 
      47            0 : #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
      48              : pub(crate) struct SecondaryLocationConfig {
      49              :     /// If true, keep the local cache warm by polling remote storage
      50              :     pub(crate) warm: bool,
      51              : }
      52              : 
      53            0 : #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
      54              : pub(crate) enum LocationMode {
      55              :     Attached(AttachedLocationConfig),
      56              :     Secondary(SecondaryLocationConfig),
      57              : }
      58              : 
      59              : /// Per-tenant, per-pageserver configuration.  All pageservers use the same TenantConf,
      60              : /// but have distinct LocationConf.
      61            0 : #[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
      62              : pub(crate) struct LocationConf {
      63              :     /// The location-specific part of the configuration, describes the operating
      64              :     /// mode of this pageserver for this tenant.
      65              :     pub(crate) mode: LocationMode,
      66              : 
      67              :     /// The detailed shard identity.  This structure is already scoped within
      68              :     /// a TenantShardId, but we need the full ShardIdentity to enable calculating
      69              :     /// key->shard mappings.
      70              :     #[serde(default = "ShardIdentity::unsharded")]
      71              :     #[serde(skip_serializing_if = "ShardIdentity::is_unsharded")]
      72              :     pub(crate) shard: ShardIdentity,
      73              : 
      74              :     /// The pan-cluster tenant configuration, the same on all locations
      75              :     pub(crate) tenant_conf: TenantConfOpt,
      76              : }
      77              : 
      78              : impl std::fmt::Debug for LocationConf {
      79            0 :     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
      80            0 :         match &self.mode {
      81            0 :             LocationMode::Attached(conf) => {
      82            0 :                 write!(
      83            0 :                     f,
      84            0 :                     "Attached {:?}, gen={:?}",
      85            0 :                     conf.attach_mode, conf.generation
      86            0 :                 )
      87              :             }
      88            0 :             LocationMode::Secondary(conf) => {
      89            0 :                 write!(f, "Secondary, warm={}", conf.warm)
      90              :             }
      91              :         }
      92            0 :     }
      93              : }
      94              : 
      95              : impl AttachedLocationConfig {
      96              :     /// Consult attachment mode to determine whether we are currently permitted
      97              :     /// to delete layers.  This is only advisory, not required for data safety.
      98              :     /// See [`AttachmentMode`] for more context.
      99          754 :     pub(crate) fn may_delete_layers_hint(&self) -> bool {
     100          754 :         // TODO: add an override for disk pressure in AttachedLocationConfig,
     101          754 :         // and respect it here.
     102          754 :         match &self.attach_mode {
     103          754 :             AttachmentMode::Single => true,
     104              :             AttachmentMode::Multi | AttachmentMode::Stale => {
     105              :                 // In Multi mode we avoid doing deletions because some other
     106              :                 // attached pageserver might get 404 while trying to read
     107              :                 // a layer we delete which is still referenced in their metadata.
     108              :                 //
     109              :                 // In Stale mode, we avoid doing deletions because we expect
     110              :                 // that they would ultimately fail validation in the deletion
     111              :                 // queue due to our stale generation.
     112            0 :                 false
     113              :             }
     114              :         }
     115          754 :     }
     116              : 
     117              :     /// Whether we are currently hinted that it is worthwhile to upload layers.
     118              :     /// This is only advisory, not required for data safety.
     119              :     /// See [`AttachmentMode`] for more context.
     120            0 :     pub(crate) fn may_upload_layers_hint(&self) -> bool {
     121            0 :         // TODO: add an override for disk pressure in AttachedLocationConfig,
     122            0 :         // and respect it here.
     123            0 :         match &self.attach_mode {
     124            0 :             AttachmentMode::Single | AttachmentMode::Multi => true,
     125              :             AttachmentMode::Stale => {
     126              :                 // In Stale mode, we avoid doing uploads because we expect that
     127              :                 // our replacement pageserver will already have started its own
     128              :                 // IndexPart that will never reference layers we upload: it is
     129              :                 // wasteful.
     130            0 :                 false
     131              :             }
     132              :         }
     133            0 :     }
     134              : }
     135              : 
     136              : impl LocationConf {
     137              :     /// For use when loading from a legacy configuration: presence of a tenant
     138              :     /// implies it is in AttachmentMode::Single, which used to be the only
     139              :     /// possible state.  This function should eventually be removed.
     140          190 :     pub(crate) fn attached_single(
     141          190 :         tenant_conf: TenantConfOpt,
     142          190 :         generation: Generation,
     143          190 :         shard_params: &models::ShardParameters,
     144          190 :     ) -> Self {
     145          190 :         Self {
     146          190 :             mode: LocationMode::Attached(AttachedLocationConfig {
     147          190 :                 generation,
     148          190 :                 attach_mode: AttachmentMode::Single,
     149          190 :             }),
     150          190 :             shard: ShardIdentity::from_params(ShardNumber(0), shard_params),
     151          190 :             tenant_conf,
     152          190 :         }
     153          190 :     }
     154              : 
     155              :     /// For use when attaching/re-attaching: update the generation stored in this
     156              :     /// structure.  If we were in a secondary state, promote to attached (posession
     157              :     /// of a fresh generation implies this).
     158            0 :     pub(crate) fn attach_in_generation(&mut self, mode: AttachmentMode, generation: Generation) {
     159            0 :         match &mut self.mode {
     160            0 :             LocationMode::Attached(attach_conf) => {
     161            0 :                 attach_conf.generation = generation;
     162            0 :                 attach_conf.attach_mode = mode;
     163            0 :             }
     164              :             LocationMode::Secondary(_) => {
     165              :                 // We are promoted to attached by the control plane's re-attach response
     166            0 :                 self.mode = LocationMode::Attached(AttachedLocationConfig {
     167            0 :                     generation,
     168            0 :                     attach_mode: mode,
     169            0 :                 })
     170              :             }
     171              :         }
     172            0 :     }
     173              : 
     174            0 :     pub(crate) fn try_from(conf: &'_ models::LocationConfig) -> anyhow::Result<Self> {
     175            0 :         let tenant_conf = TenantConfOpt::try_from(&conf.tenant_conf)?;
     176              : 
     177            0 :         fn get_generation(conf: &'_ models::LocationConfig) -> Result<Generation, anyhow::Error> {
     178            0 :             conf.generation
     179            0 :                 .map(Generation::new)
     180            0 :                 .ok_or_else(|| anyhow::anyhow!("Generation must be set when attaching"))
     181            0 :         }
     182              : 
     183            0 :         let mode = match &conf.mode {
     184              :             models::LocationConfigMode::AttachedMulti => {
     185              :                 LocationMode::Attached(AttachedLocationConfig {
     186            0 :                     generation: get_generation(conf)?,
     187            0 :                     attach_mode: AttachmentMode::Multi,
     188              :                 })
     189              :             }
     190              :             models::LocationConfigMode::AttachedSingle => {
     191              :                 LocationMode::Attached(AttachedLocationConfig {
     192            0 :                     generation: get_generation(conf)?,
     193            0 :                     attach_mode: AttachmentMode::Single,
     194              :                 })
     195              :             }
     196              :             models::LocationConfigMode::AttachedStale => {
     197              :                 LocationMode::Attached(AttachedLocationConfig {
     198            0 :                     generation: get_generation(conf)?,
     199            0 :                     attach_mode: AttachmentMode::Stale,
     200              :                 })
     201              :             }
     202              :             models::LocationConfigMode::Secondary => {
     203            0 :                 anyhow::ensure!(conf.generation.is_none());
     204              : 
     205            0 :                 let warm = conf
     206            0 :                     .secondary_conf
     207            0 :                     .as_ref()
     208            0 :                     .map(|c| c.warm)
     209            0 :                     .unwrap_or(false);
     210            0 :                 LocationMode::Secondary(SecondaryLocationConfig { warm })
     211              :             }
     212              :             models::LocationConfigMode::Detached => {
     213              :                 // Should not have been called: API code should translate this mode
     214              :                 // into a detach rather than trying to decode it as a LocationConf
     215            0 :                 return Err(anyhow::anyhow!("Cannot decode a Detached configuration"));
     216              :             }
     217              :         };
     218              : 
     219            0 :         let shard = if conf.shard_count == 0 {
     220            0 :             ShardIdentity::unsharded()
     221              :         } else {
     222            0 :             ShardIdentity::new(
     223            0 :                 ShardNumber(conf.shard_number),
     224            0 :                 ShardCount::new(conf.shard_count),
     225            0 :                 ShardStripeSize(conf.shard_stripe_size),
     226            0 :             )?
     227              :         };
     228              : 
     229            0 :         Ok(Self {
     230            0 :             shard,
     231            0 :             mode,
     232            0 :             tenant_conf,
     233            0 :         })
     234            0 :     }
     235              : }
     236              : 
     237              : impl Default for LocationConf {
     238              :     // TODO: this should be removed once tenant loading can guarantee that we are never
     239              :     // loading from a directory without a configuration.
     240              :     // => tech debt since https://github.com/neondatabase/neon/issues/1555
     241            0 :     fn default() -> Self {
     242            0 :         Self {
     243            0 :             mode: LocationMode::Attached(AttachedLocationConfig {
     244            0 :                 generation: Generation::none(),
     245            0 :                 attach_mode: AttachmentMode::Single,
     246            0 :             }),
     247            0 :             tenant_conf: TenantConfOpt::default(),
     248            0 :             shard: ShardIdentity::unsharded(),
     249            0 :         }
     250            0 :     }
     251              : }
     252              : 
     253              : /// Same as TenantConf, but this struct preserves the information about
     254              : /// which parameters are set and which are not.
     255          120 : #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
     256              : pub struct TenantConfOpt {
     257              :     #[serde(skip_serializing_if = "Option::is_none")]
     258              :     #[serde(default)]
     259              :     pub checkpoint_distance: Option<u64>,
     260              : 
     261              :     #[serde(skip_serializing_if = "Option::is_none")]
     262              :     #[serde(with = "humantime_serde")]
     263              :     #[serde(default)]
     264              :     pub checkpoint_timeout: Option<Duration>,
     265              : 
     266              :     #[serde(skip_serializing_if = "Option::is_none")]
     267              :     #[serde(default)]
     268              :     pub compaction_target_size: Option<u64>,
     269              : 
     270              :     #[serde(skip_serializing_if = "Option::is_none")]
     271              :     #[serde(with = "humantime_serde")]
     272              :     #[serde(default)]
     273              :     pub compaction_period: Option<Duration>,
     274              : 
     275              :     #[serde(skip_serializing_if = "Option::is_none")]
     276              :     #[serde(default)]
     277              :     pub compaction_threshold: Option<usize>,
     278              : 
     279              :     #[serde(skip_serializing_if = "Option::is_none")]
     280              :     #[serde(default)]
     281              :     pub compaction_algorithm: Option<CompactionAlgorithmSettings>,
     282              : 
     283              :     #[serde(skip_serializing_if = "Option::is_none")]
     284              :     #[serde(default)]
     285              :     pub gc_horizon: Option<u64>,
     286              : 
     287              :     #[serde(skip_serializing_if = "Option::is_none")]
     288              :     #[serde(with = "humantime_serde")]
     289              :     #[serde(default)]
     290              :     pub gc_period: Option<Duration>,
     291              : 
     292              :     #[serde(skip_serializing_if = "Option::is_none")]
     293              :     #[serde(default)]
     294              :     pub image_creation_threshold: Option<usize>,
     295              : 
     296              :     #[serde(skip_serializing_if = "Option::is_none")]
     297              :     #[serde(with = "humantime_serde")]
     298              :     #[serde(default)]
     299              :     pub pitr_interval: Option<Duration>,
     300              : 
     301              :     #[serde(skip_serializing_if = "Option::is_none")]
     302              :     #[serde(with = "humantime_serde")]
     303              :     #[serde(default)]
     304              :     pub walreceiver_connect_timeout: Option<Duration>,
     305              : 
     306              :     #[serde(skip_serializing_if = "Option::is_none")]
     307              :     #[serde(with = "humantime_serde")]
     308              :     #[serde(default)]
     309              :     pub lagging_wal_timeout: Option<Duration>,
     310              : 
     311              :     #[serde(skip_serializing_if = "Option::is_none")]
     312              :     #[serde(default)]
     313              :     pub max_lsn_wal_lag: Option<NonZeroU64>,
     314              : 
     315              :     #[serde(skip_serializing_if = "Option::is_none")]
     316              :     #[serde(default)]
     317              :     pub eviction_policy: Option<EvictionPolicy>,
     318              : 
     319              :     #[serde(skip_serializing_if = "Option::is_none")]
     320              :     #[serde(default)]
     321              :     pub min_resident_size_override: Option<u64>,
     322              : 
     323              :     #[serde(skip_serializing_if = "Option::is_none")]
     324              :     #[serde(with = "humantime_serde")]
     325              :     #[serde(default)]
     326              :     pub evictions_low_residence_duration_metric_threshold: Option<Duration>,
     327              : 
     328              :     #[serde(skip_serializing_if = "Option::is_none")]
     329              :     #[serde(with = "humantime_serde")]
     330              :     #[serde(default)]
     331              :     pub heatmap_period: Option<Duration>,
     332              : 
     333              :     #[serde(skip_serializing_if = "Option::is_none")]
     334              :     #[serde(default)]
     335              :     pub lazy_slru_download: Option<bool>,
     336              : 
     337              :     #[serde(skip_serializing_if = "Option::is_none")]
     338              :     pub timeline_get_throttle: Option<pageserver_api::models::ThrottleConfig>,
     339              : 
     340              :     #[serde(skip_serializing_if = "Option::is_none")]
     341              :     pub image_layer_creation_check_threshold: Option<u8>,
     342              : 
     343              :     #[serde(skip_serializing_if = "Option::is_none")]
     344              :     #[serde(with = "humantime_serde")]
     345              :     #[serde(default)]
     346              :     pub lsn_lease_length: Option<Duration>,
     347              : 
     348              :     #[serde(skip_serializing_if = "Option::is_none")]
     349              :     #[serde(with = "humantime_serde")]
     350              :     #[serde(default)]
     351              :     pub lsn_lease_length_for_ts: Option<Duration>,
     352              : 
     353              :     #[serde(skip_serializing_if = "Option::is_none")]
     354              :     #[serde(default)]
     355              :     pub timeline_offloading: Option<bool>,
     356              : }
     357              : 
     358              : impl TenantConfOpt {
     359            0 :     pub fn merge(&self, global_conf: TenantConf) -> TenantConf {
     360            0 :         TenantConf {
     361            0 :             checkpoint_distance: self
     362            0 :                 .checkpoint_distance
     363            0 :                 .unwrap_or(global_conf.checkpoint_distance),
     364            0 :             checkpoint_timeout: self
     365            0 :                 .checkpoint_timeout
     366            0 :                 .unwrap_or(global_conf.checkpoint_timeout),
     367            0 :             compaction_target_size: self
     368            0 :                 .compaction_target_size
     369            0 :                 .unwrap_or(global_conf.compaction_target_size),
     370            0 :             compaction_period: self
     371            0 :                 .compaction_period
     372            0 :                 .unwrap_or(global_conf.compaction_period),
     373            0 :             compaction_threshold: self
     374            0 :                 .compaction_threshold
     375            0 :                 .unwrap_or(global_conf.compaction_threshold),
     376            0 :             compaction_algorithm: self
     377            0 :                 .compaction_algorithm
     378            0 :                 .as_ref()
     379            0 :                 .unwrap_or(&global_conf.compaction_algorithm)
     380            0 :                 .clone(),
     381            0 :             gc_horizon: self.gc_horizon.unwrap_or(global_conf.gc_horizon),
     382            0 :             gc_period: self.gc_period.unwrap_or(global_conf.gc_period),
     383            0 :             image_creation_threshold: self
     384            0 :                 .image_creation_threshold
     385            0 :                 .unwrap_or(global_conf.image_creation_threshold),
     386            0 :             pitr_interval: self.pitr_interval.unwrap_or(global_conf.pitr_interval),
     387            0 :             walreceiver_connect_timeout: self
     388            0 :                 .walreceiver_connect_timeout
     389            0 :                 .unwrap_or(global_conf.walreceiver_connect_timeout),
     390            0 :             lagging_wal_timeout: self
     391            0 :                 .lagging_wal_timeout
     392            0 :                 .unwrap_or(global_conf.lagging_wal_timeout),
     393            0 :             max_lsn_wal_lag: self.max_lsn_wal_lag.unwrap_or(global_conf.max_lsn_wal_lag),
     394            0 :             eviction_policy: self.eviction_policy.unwrap_or(global_conf.eviction_policy),
     395            0 :             min_resident_size_override: self
     396            0 :                 .min_resident_size_override
     397            0 :                 .or(global_conf.min_resident_size_override),
     398            0 :             evictions_low_residence_duration_metric_threshold: self
     399            0 :                 .evictions_low_residence_duration_metric_threshold
     400            0 :                 .unwrap_or(global_conf.evictions_low_residence_duration_metric_threshold),
     401            0 :             heatmap_period: self.heatmap_period.unwrap_or(global_conf.heatmap_period),
     402            0 :             lazy_slru_download: self
     403            0 :                 .lazy_slru_download
     404            0 :                 .unwrap_or(global_conf.lazy_slru_download),
     405            0 :             timeline_get_throttle: self
     406            0 :                 .timeline_get_throttle
     407            0 :                 .clone()
     408            0 :                 .unwrap_or(global_conf.timeline_get_throttle),
     409            0 :             image_layer_creation_check_threshold: self
     410            0 :                 .image_layer_creation_check_threshold
     411            0 :                 .unwrap_or(global_conf.image_layer_creation_check_threshold),
     412            0 :             lsn_lease_length: self
     413            0 :                 .lsn_lease_length
     414            0 :                 .unwrap_or(global_conf.lsn_lease_length),
     415            0 :             lsn_lease_length_for_ts: self
     416            0 :                 .lsn_lease_length_for_ts
     417            0 :                 .unwrap_or(global_conf.lsn_lease_length_for_ts),
     418            0 :             timeline_offloading: self
     419            0 :                 .lazy_slru_download
     420            0 :                 .unwrap_or(global_conf.timeline_offloading),
     421            0 :         }
     422            0 :     }
     423              : }
     424              : 
     425              : impl TryFrom<&'_ models::TenantConfig> for TenantConfOpt {
     426              :     type Error = anyhow::Error;
     427              : 
     428            4 :     fn try_from(request_data: &'_ models::TenantConfig) -> Result<Self, Self::Error> {
     429              :         // Convert the request_data to a JSON Value
     430            4 :         let json_value: Value = serde_json::to_value(request_data)?;
     431              : 
     432              :         // Create a Deserializer from the JSON Value
     433            4 :         let deserializer = json_value.into_deserializer();
     434              : 
     435              :         // Use serde_path_to_error to deserialize the JSON Value into TenantConfOpt
     436            4 :         let tenant_conf: TenantConfOpt = serde_path_to_error::deserialize(deserializer)?;
     437              : 
     438            2 :         Ok(tenant_conf)
     439            4 :     }
     440              : }
     441              : 
     442              : /// This is a conversion from our internal tenant config object to the one used
     443              : /// in external APIs.
     444              : impl From<TenantConfOpt> for models::TenantConfig {
     445            0 :     fn from(value: TenantConfOpt) -> Self {
     446            0 :         fn humantime(d: Duration) -> String {
     447            0 :             format!("{}s", d.as_secs())
     448            0 :         }
     449            0 :         Self {
     450            0 :             checkpoint_distance: value.checkpoint_distance,
     451            0 :             checkpoint_timeout: value.checkpoint_timeout.map(humantime),
     452            0 :             compaction_algorithm: value.compaction_algorithm,
     453            0 :             compaction_target_size: value.compaction_target_size,
     454            0 :             compaction_period: value.compaction_period.map(humantime),
     455            0 :             compaction_threshold: value.compaction_threshold,
     456            0 :             gc_horizon: value.gc_horizon,
     457            0 :             gc_period: value.gc_period.map(humantime),
     458            0 :             image_creation_threshold: value.image_creation_threshold,
     459            0 :             pitr_interval: value.pitr_interval.map(humantime),
     460            0 :             walreceiver_connect_timeout: value.walreceiver_connect_timeout.map(humantime),
     461            0 :             lagging_wal_timeout: value.lagging_wal_timeout.map(humantime),
     462            0 :             max_lsn_wal_lag: value.max_lsn_wal_lag,
     463            0 :             eviction_policy: value.eviction_policy,
     464            0 :             min_resident_size_override: value.min_resident_size_override,
     465            0 :             evictions_low_residence_duration_metric_threshold: value
     466            0 :                 .evictions_low_residence_duration_metric_threshold
     467            0 :                 .map(humantime),
     468            0 :             heatmap_period: value.heatmap_period.map(humantime),
     469            0 :             lazy_slru_download: value.lazy_slru_download,
     470            0 :             timeline_get_throttle: value.timeline_get_throttle.map(ThrottleConfig::from),
     471            0 :             image_layer_creation_check_threshold: value.image_layer_creation_check_threshold,
     472            0 :             lsn_lease_length: value.lsn_lease_length.map(humantime),
     473            0 :             lsn_lease_length_for_ts: value.lsn_lease_length_for_ts.map(humantime),
     474            0 :             timeline_offloading: value.timeline_offloading,
     475            0 :         }
     476            0 :     }
     477              : }
     478              : 
     479              : #[cfg(test)]
     480              : mod tests {
     481              :     use super::*;
     482              :     use models::TenantConfig;
     483              : 
     484              :     #[test]
     485            2 :     fn de_serializing_pageserver_config_omits_empty_values() {
     486            2 :         let small_conf = TenantConfOpt {
     487            2 :             gc_horizon: Some(42),
     488            2 :             ..TenantConfOpt::default()
     489            2 :         };
     490            2 : 
     491            2 :         let toml_form = toml_edit::ser::to_string(&small_conf).unwrap();
     492            2 :         assert_eq!(toml_form, "gc_horizon = 42\n");
     493            2 :         assert_eq!(small_conf, toml_edit::de::from_str(&toml_form).unwrap());
     494              : 
     495            2 :         let json_form = serde_json::to_string(&small_conf).unwrap();
     496            2 :         assert_eq!(json_form, "{\"gc_horizon\":42}");
     497            2 :         assert_eq!(small_conf, serde_json::from_str(&json_form).unwrap());
     498            2 :     }
     499              : 
     500              :     #[test]
     501            2 :     fn test_try_from_models_tenant_config_err() {
     502            2 :         let tenant_config = models::TenantConfig {
     503            2 :             lagging_wal_timeout: Some("5a".to_string()),
     504            2 :             ..TenantConfig::default()
     505            2 :         };
     506            2 : 
     507            2 :         let tenant_conf_opt = TenantConfOpt::try_from(&tenant_config);
     508            2 : 
     509            2 :         assert!(
     510            2 :             tenant_conf_opt.is_err(),
     511            0 :             "Suceeded to convert TenantConfig to TenantConfOpt"
     512              :         );
     513              : 
     514            2 :         let expected_error_str =
     515            2 :             "lagging_wal_timeout: invalid value: string \"5a\", expected a duration";
     516            2 :         assert_eq!(tenant_conf_opt.unwrap_err().to_string(), expected_error_str);
     517            2 :     }
     518              : 
     519              :     #[test]
     520            2 :     fn test_try_from_models_tenant_config_success() {
     521            2 :         let tenant_config = models::TenantConfig {
     522            2 :             lagging_wal_timeout: Some("5s".to_string()),
     523            2 :             ..TenantConfig::default()
     524            2 :         };
     525            2 : 
     526            2 :         let tenant_conf_opt = TenantConfOpt::try_from(&tenant_config).unwrap();
     527            2 : 
     528            2 :         assert_eq!(
     529            2 :             tenant_conf_opt.lagging_wal_timeout,
     530            2 :             Some(Duration::from_secs(5))
     531            2 :         );
     532            2 :     }
     533              : }
        

Generated by: LCOV version 2.1-beta