LCOV - code coverage report
Current view: top level - pageserver/src/tenant - config.rs (source / functions) Coverage Total Hit
Test: 1e20c4f2b28aa592527961bb32170ebbd2c9172f.info Lines: 29.3 % 116 34
Test Date: 2025-07-16 12:29:03 Functions: 15.4 % 26 4

            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              : 
      12              : use pageserver_api::models;
      13              : use pageserver_api::shard::{ShardCount, ShardIdentity, ShardNumber, ShardStripeSize};
      14              : use serde::{Deserialize, Serialize};
      15              : use utils::critical;
      16              : use utils::generation::Generation;
      17              : 
      18            0 : #[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
      19              : pub(crate) enum AttachmentMode {
      20              :     /// Our generation is current as far as we know, and as far as we know we are the only attached
      21              :     /// pageserver.  This is the "normal" attachment mode.
      22              :     Single,
      23              :     /// Our generation number is current as far as we know, but we are advised that another
      24              :     /// pageserver is still attached, and therefore to avoid executing deletions.   This is
      25              :     /// the attachment mode of a pagesever that is the destination of a migration.
      26              :     Multi,
      27              :     /// Our generation number is superseded, or about to be superseded.  We are advised
      28              :     /// to avoid remote storage writes if possible, and to avoid sending billing data.  This
      29              :     /// is the attachment mode of a pageserver that is the origin of a migration.
      30              :     Stale,
      31              : }
      32              : 
      33            0 : #[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
      34              : pub(crate) struct AttachedLocationConfig {
      35              :     pub(crate) generation: Generation,
      36              :     pub(crate) attach_mode: AttachmentMode,
      37              :     // TODO: add a flag to override AttachmentMode's policies under
      38              :     // disk pressure (i.e. unblock uploads under disk pressure in Stale
      39              :     // state, unblock deletions after timeout in Multi state)
      40              : }
      41              : 
      42            0 : #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
      43              : pub(crate) struct SecondaryLocationConfig {
      44              :     /// If true, keep the local cache warm by polling remote storage
      45              :     pub(crate) warm: bool,
      46              : }
      47              : 
      48            0 : #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
      49              : pub(crate) enum LocationMode {
      50              :     Attached(AttachedLocationConfig),
      51              :     Secondary(SecondaryLocationConfig),
      52              : }
      53              : 
      54              : /// Per-tenant, per-pageserver configuration.  All pageservers use the same TenantConf,
      55              : /// but have distinct LocationConf.
      56            0 : #[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
      57              : pub(crate) struct LocationConf {
      58              :     /// The location-specific part of the configuration, describes the operating
      59              :     /// mode of this pageserver for this tenant.
      60              :     pub(crate) mode: LocationMode,
      61              : 
      62              :     /// The detailed shard identity.  This structure is already scoped within
      63              :     /// a TenantShardId, but we need the full ShardIdentity to enable calculating
      64              :     /// key->shard mappings.
      65              :     ///
      66              :     /// NB: we store this even for unsharded tenants, so that we agree with storcon on the intended
      67              :     /// stripe size. Otherwise, a split request that does not specify a stripe size may use a
      68              :     /// different default than storcon, which can lead to incorrect stripe sizes and corruption.
      69              :     pub(crate) shard: ShardIdentity,
      70              : 
      71              :     /// The pan-cluster tenant configuration, the same on all locations
      72              :     pub(crate) tenant_conf: pageserver_api::models::TenantConfig,
      73              : }
      74              : 
      75              : impl std::fmt::Debug for LocationConf {
      76            0 :     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
      77            0 :         match &self.mode {
      78            0 :             LocationMode::Attached(conf) => {
      79            0 :                 write!(
      80            0 :                     f,
      81            0 :                     "Attached {:?}, gen={:?}",
      82              :                     conf.attach_mode, conf.generation
      83              :                 )
      84              :             }
      85            0 :             LocationMode::Secondary(conf) => {
      86            0 :                 write!(f, "Secondary, warm={}", conf.warm)
      87              :             }
      88              :         }
      89            0 :     }
      90              : }
      91              : 
      92              : impl AttachedLocationConfig {
      93              :     /// Consult attachment mode to determine whether we are currently permitted
      94              :     /// to delete layers.  This is only advisory, not required for data safety.
      95              :     /// See [`AttachmentMode`] for more context.
      96          617 :     pub(crate) fn may_delete_layers_hint(&self) -> bool {
      97              :         // TODO: add an override for disk pressure in AttachedLocationConfig,
      98              :         // and respect it here.
      99          617 :         match &self.attach_mode {
     100          617 :             AttachmentMode::Single => true,
     101              :             AttachmentMode::Multi | AttachmentMode::Stale => {
     102              :                 // In Multi mode we avoid doing deletions because some other
     103              :                 // attached pageserver might get 404 while trying to read
     104              :                 // a layer we delete which is still referenced in their metadata.
     105              :                 //
     106              :                 // In Stale mode, we avoid doing deletions because we expect
     107              :                 // that they would ultimately fail validation in the deletion
     108              :                 // queue due to our stale generation.
     109            0 :                 false
     110              :             }
     111              :         }
     112          617 :     }
     113              : 
     114              :     /// Whether we are currently hinted that it is worthwhile to upload layers.
     115              :     /// This is only advisory, not required for data safety.
     116              :     /// See [`AttachmentMode`] for more context.
     117          240 :     pub(crate) fn may_upload_layers_hint(&self) -> bool {
     118              :         // TODO: add an override for disk pressure in AttachedLocationConfig,
     119              :         // and respect it here.
     120          240 :         match &self.attach_mode {
     121          240 :             AttachmentMode::Single | AttachmentMode::Multi => true,
     122              :             AttachmentMode::Stale => {
     123              :                 // In Stale mode, we avoid doing uploads because we expect that
     124              :                 // our replacement pageserver will already have started its own
     125              :                 // IndexPart that will never reference layers we upload: it is
     126              :                 // wasteful.
     127            0 :                 false
     128              :             }
     129              :         }
     130          240 :     }
     131              : }
     132              : 
     133              : impl LocationConf {
     134              :     /// For use when loading from a legacy configuration: presence of a tenant
     135              :     /// implies it is in AttachmentMode::Single, which used to be the only
     136              :     /// possible state.  This function should eventually be removed.
     137          119 :     pub(crate) fn attached_single(
     138          119 :         tenant_conf: pageserver_api::models::TenantConfig,
     139          119 :         generation: Generation,
     140          119 :         shard_params: models::ShardParameters,
     141          119 :     ) -> Self {
     142          119 :         Self {
     143          119 :             mode: LocationMode::Attached(AttachedLocationConfig {
     144          119 :                 generation,
     145          119 :                 attach_mode: AttachmentMode::Single,
     146          119 :             }),
     147          119 :             shard: ShardIdentity::from_params(ShardNumber(0), shard_params),
     148          119 :             tenant_conf,
     149          119 :         }
     150          119 :     }
     151              : 
     152              :     /// For use when attaching/re-attaching: update the generation stored in this
     153              :     /// structure.  If we were in a secondary state, promote to attached (posession
     154              :     /// of a fresh generation implies this).
     155            0 :     pub(crate) fn attach_in_generation(
     156            0 :         &mut self,
     157            0 :         mode: AttachmentMode,
     158            0 :         generation: Generation,
     159            0 :         stripe_size: ShardStripeSize,
     160            0 :     ) {
     161            0 :         match &mut self.mode {
     162            0 :             LocationMode::Attached(attach_conf) => {
     163            0 :                 attach_conf.generation = generation;
     164            0 :                 attach_conf.attach_mode = mode;
     165            0 :             }
     166              :             LocationMode::Secondary(_) => {
     167              :                 // We are promoted to attached by the control plane's re-attach response
     168            0 :                 self.mode = LocationMode::Attached(AttachedLocationConfig {
     169            0 :                     generation,
     170            0 :                     attach_mode: mode,
     171            0 :                 })
     172              :             }
     173              :         }
     174              : 
     175              :         // This should never happen.
     176              :         // TODO: turn this into a proper assertion.
     177            0 :         if stripe_size != self.shard.stripe_size {
     178            0 :             critical!(
     179            0 :                 "stripe size mismatch: {} != {}",
     180              :                 self.shard.stripe_size,
     181              :                 stripe_size,
     182              :             );
     183            0 :         }
     184              : 
     185            0 :         self.shard.stripe_size = stripe_size;
     186            0 :     }
     187              : 
     188            0 :     pub(crate) fn try_from(conf: &'_ models::LocationConfig) -> anyhow::Result<Self> {
     189            0 :         let tenant_conf = conf.tenant_conf.clone();
     190              : 
     191            0 :         fn get_generation(conf: &'_ models::LocationConfig) -> Result<Generation, anyhow::Error> {
     192            0 :             conf.generation
     193            0 :                 .map(Generation::new)
     194            0 :                 .ok_or_else(|| anyhow::anyhow!("Generation must be set when attaching"))
     195            0 :         }
     196              : 
     197            0 :         let mode = match &conf.mode {
     198              :             models::LocationConfigMode::AttachedMulti => {
     199              :                 LocationMode::Attached(AttachedLocationConfig {
     200            0 :                     generation: get_generation(conf)?,
     201            0 :                     attach_mode: AttachmentMode::Multi,
     202              :                 })
     203              :             }
     204              :             models::LocationConfigMode::AttachedSingle => {
     205              :                 LocationMode::Attached(AttachedLocationConfig {
     206            0 :                     generation: get_generation(conf)?,
     207            0 :                     attach_mode: AttachmentMode::Single,
     208              :                 })
     209              :             }
     210              :             models::LocationConfigMode::AttachedStale => {
     211              :                 LocationMode::Attached(AttachedLocationConfig {
     212            0 :                     generation: get_generation(conf)?,
     213            0 :                     attach_mode: AttachmentMode::Stale,
     214              :                 })
     215              :             }
     216              :             models::LocationConfigMode::Secondary => {
     217            0 :                 anyhow::ensure!(conf.generation.is_none());
     218              : 
     219            0 :                 let warm = conf
     220            0 :                     .secondary_conf
     221            0 :                     .as_ref()
     222            0 :                     .map(|c| c.warm)
     223            0 :                     .unwrap_or(false);
     224            0 :                 LocationMode::Secondary(SecondaryLocationConfig { warm })
     225              :             }
     226              :             models::LocationConfigMode::Detached => {
     227              :                 // Should not have been called: API code should translate this mode
     228              :                 // into a detach rather than trying to decode it as a LocationConf
     229            0 :                 return Err(anyhow::anyhow!("Cannot decode a Detached configuration"));
     230              :             }
     231              :         };
     232              : 
     233            0 :         let shard = if conf.shard_count == 0 {
     234              :             // NB: carry over the persisted stripe size instead of using the default. This doesn't
     235              :             // matter for most practical purposes, since unsharded tenants don't use the stripe
     236              :             // size, but can cause inconsistencies between storcon and Pageserver and cause manual
     237              :             // splits without `new_stripe_size` to use an unintended stripe size.
     238            0 :             ShardIdentity::unsharded_with_stripe_size(ShardStripeSize(conf.shard_stripe_size))
     239              :         } else {
     240            0 :             ShardIdentity::new(
     241            0 :                 ShardNumber(conf.shard_number),
     242            0 :                 ShardCount::new(conf.shard_count),
     243            0 :                 ShardStripeSize(conf.shard_stripe_size),
     244            0 :             )?
     245              :         };
     246              : 
     247            0 :         Ok(Self {
     248            0 :             shard,
     249            0 :             mode,
     250            0 :             tenant_conf,
     251            0 :         })
     252            0 :     }
     253              : }
     254              : 
     255              : impl Default for LocationConf {
     256              :     // TODO: this should be removed once tenant loading can guarantee that we are never
     257              :     // loading from a directory without a configuration.
     258              :     // => tech debt since https://github.com/neondatabase/neon/issues/1555
     259            0 :     fn default() -> Self {
     260            0 :         Self {
     261            0 :             mode: LocationMode::Attached(AttachedLocationConfig {
     262            0 :                 generation: Generation::none(),
     263            0 :                 attach_mode: AttachmentMode::Single,
     264            0 :             }),
     265            0 :             tenant_conf: pageserver_api::models::TenantConfig::default(),
     266            0 :             shard: ShardIdentity::unsharded(),
     267            0 :         }
     268            0 :     }
     269              : }
     270              : 
     271              : #[cfg(test)]
     272              : mod tests {
     273              :     #[test]
     274            1 :     fn serde_roundtrip_tenant_conf_opt() {
     275            1 :         let small_conf = pageserver_api::models::TenantConfig {
     276            1 :             gc_horizon: Some(42),
     277            1 :             ..Default::default()
     278            1 :         };
     279              : 
     280            1 :         let toml_form = toml_edit::ser::to_string(&small_conf).unwrap();
     281            1 :         assert_eq!(toml_form, "gc_horizon = 42\n");
     282            1 :         assert_eq!(small_conf, toml_edit::de::from_str(&toml_form).unwrap());
     283              : 
     284            1 :         let json_form = serde_json::to_string(&small_conf).unwrap();
     285            1 :         assert_eq!(json_form, "{\"gc_horizon\":42}");
     286            1 :         assert_eq!(small_conf, serde_json::from_str(&json_form).unwrap());
     287            1 :     }
     288              : }
        

Generated by: LCOV version 2.1-beta