LCOV - code coverage report
Current view: top level - pageserver/src/tenant/remote_timeline_client - manifest.rs (source / functions) Coverage Total Hit
Test: 37bd82a80da9937a25818120dcf8e865ea9f7fd2.info Lines: 91.3 % 115 105
Test Date: 2025-04-11 14:30:22 Functions: 52.9 % 17 9

            Line data    Source code
       1              : use chrono::NaiveDateTime;
       2              : use pageserver_api::shard::ShardStripeSize;
       3              : use serde::{Deserialize, Serialize};
       4              : use utils::id::TimelineId;
       5              : use utils::lsn::Lsn;
       6              : 
       7              : /// Tenant shard manifest, stored in remote storage. Contains offloaded timelines and other tenant
       8              : /// shard-wide information that must be persisted in remote storage.
       9              : ///
      10              : /// The manifest is always updated on tenant attach, and as needed.
      11           68 : #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
      12              : pub struct TenantManifest {
      13              :     /// The manifest version. Incremented on manifest format changes, even non-breaking ones.
      14              :     /// Manifests must generally always be backwards and forwards compatible for one release, to
      15              :     /// allow release rollbacks.
      16              :     pub version: usize,
      17              : 
      18              :     /// This tenant's stripe size. This is only advisory, and used to recover tenant data from
      19              :     /// remote storage. The autoritative source is the storage controller. If None, assume the
      20              :     /// original default value of 32768 blocks (256 MB).
      21              :     #[serde(skip_serializing_if = "Option::is_none")]
      22              :     pub stripe_size: Option<ShardStripeSize>,
      23              : 
      24              :     /// The list of offloaded timelines together with enough information
      25              :     /// to not have to actually load them.
      26              :     ///
      27              :     /// Note: the timelines mentioned in this list might be deleted, i.e.
      28              :     /// we don't hold an invariant that the references aren't dangling.
      29              :     /// Existence of index-part.json is the actual indicator of timeline existence.
      30              :     #[serde(default)]
      31              :     pub offloaded_timelines: Vec<OffloadedTimelineManifest>,
      32              : }
      33              : 
      34              : /// The remote level representation of an offloaded timeline.
      35              : ///
      36              : /// Very similar to [`pageserver_api::models::OffloadedTimelineInfo`],
      37              : /// but the two datastructures serve different needs, this is for a persistent disk format
      38              : /// that must be backwards compatible, while the other is only for informative purposes.
      39           48 : #[derive(Clone, Debug, Serialize, Deserialize, Copy, PartialEq, Eq)]
      40              : pub struct OffloadedTimelineManifest {
      41              :     pub timeline_id: TimelineId,
      42              :     /// Whether the timeline has a parent it has been branched off from or not
      43              :     pub ancestor_timeline_id: Option<TimelineId>,
      44              :     /// Whether to retain the branch lsn at the ancestor or not
      45              :     pub ancestor_retain_lsn: Option<Lsn>,
      46              :     /// The time point when the timeline was archived
      47              :     pub archived_at: NaiveDateTime,
      48              : }
      49              : 
      50              : /// The newest manifest version. This should be incremented on changes, even non-breaking ones. We
      51              : /// do not use deny_unknown_fields, so new fields are not breaking.
      52              : ///
      53              : /// 1: initial version
      54              : /// 2: +stripe_size
      55              : ///
      56              : /// When adding new versions, also add a parse_vX test case below.
      57              : pub const LATEST_TENANT_MANIFEST_VERSION: usize = 2;
      58              : 
      59              : impl TenantManifest {
      60              :     /// Returns true if the manifests are equal, ignoring the version number. This avoids
      61              :     /// re-uploading all manifests just because the version number is bumped.
      62           16 :     pub fn eq_ignoring_version(&self, other: &Self) -> bool {
      63           16 :         // Fast path: if the version is equal, just compare directly.
      64           16 :         if self.version == other.version {
      65           16 :             return self == other;
      66            0 :         }
      67            0 : 
      68            0 :         // We could alternatively just clone and modify the version here.
      69            0 :         let Self {
      70            0 :             version: _, // ignore version
      71            0 :             stripe_size,
      72            0 :             offloaded_timelines,
      73            0 :         } = self;
      74            0 : 
      75            0 :         stripe_size == &other.stripe_size && offloaded_timelines == &other.offloaded_timelines
      76           16 :     }
      77              : 
      78              :     /// Decodes a manifest from JSON.
      79           28 :     pub fn from_json_bytes(bytes: &[u8]) -> Result<Self, serde_json::Error> {
      80           28 :         serde_json::from_slice(bytes)
      81           28 :     }
      82              : 
      83              :     /// Encodes a manifest as JSON.
      84          452 :     pub fn to_json_bytes(&self) -> serde_json::Result<Vec<u8>> {
      85          452 :         serde_json::to_vec(self)
      86          452 :     }
      87              : }
      88              : 
      89              : #[cfg(test)]
      90              : mod tests {
      91              :     use std::str::FromStr;
      92              : 
      93              :     use utils::id::TimelineId;
      94              : 
      95              :     use super::*;
      96              : 
      97              :     /// Empty manifests should be parsed. Version is required.
      98              :     #[test]
      99            4 :     fn parse_empty() -> anyhow::Result<()> {
     100            4 :         let json = r#"{
     101            4 :              "version": 0
     102            4 :          }"#;
     103            4 :         let expected = TenantManifest {
     104            4 :             version: 0,
     105            4 :             stripe_size: None,
     106            4 :             offloaded_timelines: Vec::new(),
     107            4 :         };
     108            4 :         assert_eq!(expected, TenantManifest::from_json_bytes(json.as_bytes())?);
     109            4 :         Ok(())
     110            4 :     }
     111              : 
     112              :     /// Unknown fields should be ignored, for forwards compatibility.
     113              :     #[test]
     114            4 :     fn parse_unknown_fields() -> anyhow::Result<()> {
     115            4 :         let json = r#"{
     116            4 :              "version": 1,
     117            4 :              "foo": "bar"
     118            4 :          }"#;
     119            4 :         let expected = TenantManifest {
     120            4 :             version: 1,
     121            4 :             stripe_size: None,
     122            4 :             offloaded_timelines: Vec::new(),
     123            4 :         };
     124            4 :         assert_eq!(expected, TenantManifest::from_json_bytes(json.as_bytes())?);
     125            4 :         Ok(())
     126            4 :     }
     127              : 
     128              :     /// v1 manifests should be parsed, for backwards compatibility.
     129              :     #[test]
     130            4 :     fn parse_v1() -> anyhow::Result<()> {
     131            4 :         let json = r#"{
     132            4 :              "version": 1,
     133            4 :              "offloaded_timelines": [
     134            4 :                  {
     135            4 :                      "timeline_id": "5c4df612fd159e63c1b7853fe94d97da",
     136            4 :                      "archived_at": "2025-03-07T11:07:11.373105434"
     137            4 :                  },
     138            4 :                  {
     139            4 :                      "timeline_id": "f3def5823ad7080d2ea538d8e12163fa",
     140            4 :                      "ancestor_timeline_id": "5c4df612fd159e63c1b7853fe94d97da",
     141            4 :                      "ancestor_retain_lsn": "0/1F79038",
     142            4 :                      "archived_at": "2025-03-05T11:10:22.257901390"
     143            4 :                  }
     144            4 :              ]
     145            4 :          }"#;
     146            4 :         let expected = TenantManifest {
     147              :             version: 1,
     148            4 :             stripe_size: None,
     149            4 :             offloaded_timelines: vec![
     150            4 :                 OffloadedTimelineManifest {
     151            4 :                     timeline_id: TimelineId::from_str("5c4df612fd159e63c1b7853fe94d97da")?,
     152            4 :                     ancestor_timeline_id: None,
     153            4 :                     ancestor_retain_lsn: None,
     154            4 :                     archived_at: NaiveDateTime::from_str("2025-03-07T11:07:11.373105434")?,
     155              :                 },
     156              :                 OffloadedTimelineManifest {
     157            4 :                     timeline_id: TimelineId::from_str("f3def5823ad7080d2ea538d8e12163fa")?,
     158            4 :                     ancestor_timeline_id: Some(TimelineId::from_str(
     159            4 :                         "5c4df612fd159e63c1b7853fe94d97da",
     160            4 :                     )?),
     161            4 :                     ancestor_retain_lsn: Some(Lsn::from_str("0/1F79038")?),
     162            4 :                     archived_at: NaiveDateTime::from_str("2025-03-05T11:10:22.257901390")?,
     163              :                 },
     164              :             ],
     165              :         };
     166            4 :         assert_eq!(expected, TenantManifest::from_json_bytes(json.as_bytes())?);
     167            4 :         Ok(())
     168            4 :     }
     169              : 
     170              :     /// v2 manifests should be parsed, for backwards compatibility.
     171              :     #[test]
     172            4 :     fn parse_v2() -> anyhow::Result<()> {
     173            4 :         let json = r#"{
     174            4 :              "version": 2,
     175            4 :              "stripe_size": 32768,
     176            4 :              "offloaded_timelines": [
     177            4 :                  {
     178            4 :                      "timeline_id": "5c4df612fd159e63c1b7853fe94d97da",
     179            4 :                      "archived_at": "2025-03-07T11:07:11.373105434"
     180            4 :                  },
     181            4 :                  {
     182            4 :                      "timeline_id": "f3def5823ad7080d2ea538d8e12163fa",
     183            4 :                      "ancestor_timeline_id": "5c4df612fd159e63c1b7853fe94d97da",
     184            4 :                      "ancestor_retain_lsn": "0/1F79038",
     185            4 :                      "archived_at": "2025-03-05T11:10:22.257901390"
     186            4 :                  }
     187            4 :              ]
     188            4 :          }"#;
     189            4 :         let expected = TenantManifest {
     190              :             version: 2,
     191            4 :             stripe_size: Some(ShardStripeSize(32768)),
     192            4 :             offloaded_timelines: vec![
     193            4 :                 OffloadedTimelineManifest {
     194            4 :                     timeline_id: TimelineId::from_str("5c4df612fd159e63c1b7853fe94d97da")?,
     195            4 :                     ancestor_timeline_id: None,
     196            4 :                     ancestor_retain_lsn: None,
     197            4 :                     archived_at: NaiveDateTime::from_str("2025-03-07T11:07:11.373105434")?,
     198              :                 },
     199              :                 OffloadedTimelineManifest {
     200            4 :                     timeline_id: TimelineId::from_str("f3def5823ad7080d2ea538d8e12163fa")?,
     201            4 :                     ancestor_timeline_id: Some(TimelineId::from_str(
     202            4 :                         "5c4df612fd159e63c1b7853fe94d97da",
     203            4 :                     )?),
     204            4 :                     ancestor_retain_lsn: Some(Lsn::from_str("0/1F79038")?),
     205            4 :                     archived_at: NaiveDateTime::from_str("2025-03-05T11:10:22.257901390")?,
     206              :                 },
     207              :             ],
     208              :         };
     209            4 :         assert_eq!(expected, TenantManifest::from_json_bytes(json.as_bytes())?);
     210            4 :         Ok(())
     211            4 :     }
     212              : }
        

Generated by: LCOV version 2.1-beta