LCOV - code coverage report
Current view: top level - pageserver/ctl/src - key.rs (source / functions) Coverage Total Hit
Test: 09e7485004805bd42b53a0c369170b3228136512.info Lines: 55.2 % 268 148
Test Date: 2024-11-21 18:36:18 Functions: 27.5 % 51 14

            Line data    Source code
       1              : use anyhow::Context;
       2              : use clap::Parser;
       3              : use pageserver_api::{
       4              :     key::Key,
       5              :     reltag::{BlockNumber, RelTag, SlruKind},
       6              :     shard::{ShardCount, ShardStripeSize},
       7              : };
       8              : use std::str::FromStr;
       9              : 
      10            0 : #[derive(Parser)]
      11              : pub(super) struct DescribeKeyCommand {
      12              :     /// Key material in one of the forms: hex, span attributes captured from log, reltag blocknum
      13            0 :     input: Vec<String>,
      14              : 
      15              :     /// The number of shards to calculate what Keys placement would be.
      16              :     #[arg(long)]
      17              :     shard_count: Option<CustomShardCount>,
      18              : 
      19              :     /// The sharding stripe size.
      20              :     ///
      21              :     /// The default is hardcoded. It makes no sense to provide this without providing
      22              :     /// `--shard-count`.
      23              :     #[arg(long, requires = "shard_count")]
      24              :     stripe_size: Option<u32>,
      25              : }
      26              : 
      27              : /// Sharded shard count without unsharded count, which the actual ShardCount supports.
      28              : #[derive(Clone, Copy)]
      29              : pub(super) struct CustomShardCount(std::num::NonZeroU8);
      30              : 
      31            0 : #[derive(Debug, thiserror::Error)]
      32              : pub(super) enum InvalidShardCount {
      33              :     #[error(transparent)]
      34              :     ParsingFailed(#[from] std::num::ParseIntError),
      35              :     #[error("too few shards")]
      36              :     TooFewShards,
      37              : }
      38              : 
      39              : impl FromStr for CustomShardCount {
      40              :     type Err = InvalidShardCount;
      41              : 
      42            0 :     fn from_str(s: &str) -> Result<Self, Self::Err> {
      43            0 :         let inner: std::num::NonZeroU8 = s.parse()?;
      44            0 :         if inner.get() < 2 {
      45            0 :             Err(InvalidShardCount::TooFewShards)
      46              :         } else {
      47            0 :             Ok(CustomShardCount(inner))
      48              :         }
      49            0 :     }
      50              : }
      51              : 
      52              : impl From<CustomShardCount> for ShardCount {
      53            0 :     fn from(value: CustomShardCount) -> Self {
      54            0 :         ShardCount::new(value.0.get())
      55            0 :     }
      56              : }
      57              : 
      58              : impl DescribeKeyCommand {
      59            0 :     pub(super) fn execute(self) {
      60            0 :         let DescribeKeyCommand {
      61            0 :             input,
      62            0 :             shard_count,
      63            0 :             stripe_size,
      64            0 :         } = self;
      65            0 : 
      66            0 :         let material = KeyMaterial::try_from(input.as_slice()).unwrap();
      67            0 :         let kind = material.kind();
      68            0 :         let key = Key::from(material);
      69            0 : 
      70            0 :         println!("parsed from {kind}: {key}:");
      71            0 :         println!();
      72            0 :         println!("{key:?}");
      73              : 
      74              :         macro_rules! kind_query {
      75              :             ([$($name:ident),*$(,)?]) => {{[$(kind_query!($name)),*]}};
      76              :             ($name:ident) => {{
      77              :                 let s: &'static str = stringify!($name);
      78              :                 let s = s.strip_prefix("is_").unwrap_or(s);
      79              :                 let s = s.strip_suffix("_key").unwrap_or(s);
      80              : 
      81              :                 #[allow(clippy::needless_borrow)]
      82              :                 (s, key.$name())
      83              :             }};
      84              :         }
      85              : 
      86              :         // the current characterization is a mess of these boolean queries and separate
      87              :         // "recognization". I think it accurately represents how strictly we model the Key
      88              :         // right now, but could of course be made less confusing.
      89              : 
      90            0 :         let queries = kind_query!([
      91            0 :             is_rel_block_key,
      92            0 :             is_rel_vm_block_key,
      93            0 :             is_rel_fsm_block_key,
      94            0 :             is_slru_block_key,
      95            0 :             is_inherited_key,
      96            0 :             is_rel_size_key,
      97            0 :             is_slru_segment_size_key,
      98            0 :         ]);
      99            0 : 
     100            0 :         let recognized_kind = "recognized kind";
     101            0 :         let metadata_key = "metadata key";
     102            0 :         let shard_placement = "shard placement";
     103            0 : 
     104            0 :         let longest = queries
     105            0 :             .iter()
     106            0 :             .map(|t| t.0)
     107            0 :             .chain([recognized_kind, metadata_key, shard_placement])
     108            0 :             .map(|s| s.len())
     109            0 :             .max()
     110            0 :             .unwrap();
     111            0 : 
     112            0 :         let colon = 1;
     113            0 :         let padding = 1;
     114              : 
     115            0 :         for (name, is) in queries {
     116            0 :             let width = longest - name.len() + colon + padding;
     117            0 :             println!("{}{:width$}{}", name, ":", is);
     118            0 :         }
     119              : 
     120            0 :         let width = longest - recognized_kind.len() + colon + padding;
     121            0 :         println!(
     122            0 :             "{}{:width$}{:?}",
     123            0 :             recognized_kind,
     124            0 :             ":",
     125            0 :             RecognizedKeyKind::new(key),
     126            0 :         );
     127              : 
     128            0 :         if let Some(shard_count) = shard_count {
     129            0 :             // seeing the sharding placement might be confusing, so leave it out unless shard
     130            0 :             // count was given.
     131            0 : 
     132            0 :             let stripe_size = stripe_size.map(ShardStripeSize).unwrap_or_default();
     133            0 :             println!(
     134            0 :                 "# placement with shard_count: {} and stripe_size: {}:",
     135            0 :                 shard_count.0, stripe_size.0
     136            0 :             );
     137            0 :             let width = longest - shard_placement.len() + colon + padding;
     138            0 :             println!(
     139            0 :                 "{}{:width$}{:?}",
     140            0 :                 shard_placement,
     141            0 :                 ":",
     142            0 :                 pageserver_api::shard::describe(&key, shard_count.into(), stripe_size)
     143            0 :             );
     144            0 :         }
     145            0 :     }
     146              : }
     147              : 
     148              : /// Hand-wavy "inputs we accept" for a key.
     149              : #[derive(Debug)]
     150              : pub(super) enum KeyMaterial {
     151              :     Hex(Key),
     152              :     String(SpanAttributesFromLogs),
     153              :     Split(RelTag, BlockNumber),
     154              : }
     155              : 
     156              : impl KeyMaterial {
     157            0 :     fn kind(&self) -> &'static str {
     158            0 :         match self {
     159            0 :             KeyMaterial::Hex(_) => "hex",
     160            0 :             KeyMaterial::String(_) | KeyMaterial::Split(_, _) => "split",
     161              :         }
     162            0 :     }
     163              : }
     164              : 
     165              : impl From<KeyMaterial> for Key {
     166            6 :     fn from(value: KeyMaterial) -> Self {
     167            6 :         match value {
     168            0 :             KeyMaterial::Hex(key) => key,
     169            3 :             KeyMaterial::String(SpanAttributesFromLogs(rt, blocknum))
     170            3 :             | KeyMaterial::Split(rt, blocknum) => {
     171            6 :                 pageserver_api::key::rel_block_to_key(rt, blocknum)
     172              :             }
     173              :         }
     174            6 :     }
     175              : }
     176              : 
     177              : impl<S: AsRef<str>> TryFrom<&[S]> for KeyMaterial {
     178              :     type Error = anyhow::Error;
     179              : 
     180            8 :     fn try_from(value: &[S]) -> Result<Self, Self::Error> {
     181            8 :         match value {
     182            8 :             [] => anyhow::bail!(
     183            0 :                 "need 1..N positional arguments describing the key, try hex or a log line"
     184            0 :             ),
     185            5 :             [one] => {
     186            5 :                 let one = one.as_ref();
     187            5 : 
     188            5 :                 let key = Key::from_hex(one).map(KeyMaterial::Hex);
     189            5 : 
     190            5 :                 let attrs = SpanAttributesFromLogs::from_str(one).map(KeyMaterial::String);
     191            5 : 
     192            5 :                 match (key, attrs) {
     193            1 :                     (Ok(key), _) => Ok(key),
     194            3 :                     (_, Ok(s)) => Ok(s),
     195            1 :                     (Err(e1), Err(e2)) => anyhow::bail!(
     196            1 :                         "failed to parse {one:?} as hex or span attributes:\n- {e1:#}\n- {e2:#}"
     197            1 :                     ),
     198              :                 }
     199              :             }
     200            3 :             more => {
     201              :                 // assume going left to right one of these is a reltag and then we find a blocknum
     202              :                 // this works, because we don't have plain numbers at least right after reltag in
     203              :                 // logs. for some definition of "works".
     204              : 
     205            3 :                 let Some((reltag_at, reltag)) = more
     206            3 :                     .iter()
     207            3 :                     .map(AsRef::as_ref)
     208            3 :                     .enumerate()
     209            4 :                     .find_map(|(i, s)| {
     210            4 :                         s.split_once("rel=")
     211            4 :                             .map(|(_garbage, actual)| actual)
     212            4 :                             .unwrap_or(s)
     213            4 :                             .parse::<RelTag>()
     214            4 :                             .ok()
     215            4 :                             .map(|rt| (i, rt))
     216            4 :                     })
     217              :                 else {
     218            0 :                     anyhow::bail!("found no RelTag in arguments");
     219              :                 };
     220              : 
     221            3 :                 let Some(blocknum) = more
     222            3 :                     .iter()
     223            3 :                     .map(AsRef::as_ref)
     224            3 :                     .skip(reltag_at)
     225            6 :                     .find_map(|s| {
     226            6 :                         s.split_once("blkno=")
     227            6 :                             .map(|(_garbage, actual)| actual)
     228            6 :                             .unwrap_or(s)
     229            6 :                             .parse::<BlockNumber>()
     230            6 :                             .ok()
     231            6 :                     })
     232              :                 else {
     233            0 :                     anyhow::bail!("found no blocknum in arguments");
     234              :                 };
     235              : 
     236            3 :                 Ok(KeyMaterial::Split(reltag, blocknum))
     237              :             }
     238              :         }
     239            8 :     }
     240              : }
     241              : 
     242              : #[derive(Debug)]
     243              : pub(super) struct SpanAttributesFromLogs(RelTag, BlockNumber);
     244              : 
     245              : impl std::str::FromStr for SpanAttributesFromLogs {
     246              :     type Err = anyhow::Error;
     247              : 
     248            5 :     fn from_str(s: &str) -> Result<Self, Self::Err> {
     249              :         // accept the span separator but do not require or fail if either is missing
     250              :         // "whatever{rel=1663/16389/24615 blkno=1052204 req_lsn=FFFFFFFF/FFFFFFFF}"
     251            5 :         let (_, reltag) = s
     252            5 :             .split_once("rel=")
     253            5 :             .ok_or_else(|| anyhow::anyhow!("cannot find 'rel='"))?;
     254            3 :         let reltag = reltag.split_whitespace().next().unwrap();
     255              : 
     256            3 :         let (_, blocknum) = s
     257            3 :             .split_once("blkno=")
     258            3 :             .ok_or_else(|| anyhow::anyhow!("cannot find 'blkno='"))?;
     259            3 :         let blocknum = blocknum.split_whitespace().next().unwrap();
     260              : 
     261            3 :         let reltag = reltag
     262            3 :             .parse()
     263            3 :             .with_context(|| format!("parse reltag from {reltag:?}"))?;
     264            3 :         let blocknum = blocknum
     265            3 :             .parse()
     266            3 :             .with_context(|| format!("parse blocknum from {blocknum:?}"))?;
     267              : 
     268            3 :         Ok(Self(reltag, blocknum))
     269            5 :     }
     270              : }
     271              : 
     272              : #[derive(Debug)]
     273              : #[allow(dead_code)] // debug print is used
     274              : enum RecognizedKeyKind {
     275              :     DbDir,
     276              :     ControlFile,
     277              :     Checkpoint,
     278              :     AuxFilesV1,
     279              :     SlruDir(Result<SlruKind, u32>),
     280              :     RelMap(RelTagish<2>),
     281              :     RelDir(RelTagish<2>),
     282              :     AuxFileV2(Result<AuxFileV2, utils::Hex<[u8; 16]>>),
     283              : }
     284              : 
     285              : #[derive(Debug, PartialEq)]
     286              : #[allow(unused)]
     287              : enum AuxFileV2 {
     288              :     Recognized(&'static str, utils::Hex<[u8; 13]>),
     289              :     OtherWithPrefix(&'static str, utils::Hex<[u8; 13]>),
     290              :     Other(utils::Hex<[u8; 13]>),
     291              : }
     292              : 
     293              : impl RecognizedKeyKind {
     294            0 :     fn new(key: Key) -> Option<Self> {
     295              :         use RecognizedKeyKind::{
     296              :             AuxFilesV1, Checkpoint, ControlFile, DbDir, RelDir, RelMap, SlruDir,
     297              :         };
     298              : 
     299            0 :         let slru_dir_kind = pageserver_api::key::slru_dir_kind(&key);
     300              : 
     301            0 :         Some(match key {
     302            0 :             pageserver_api::key::DBDIR_KEY => DbDir,
     303            0 :             pageserver_api::key::CONTROLFILE_KEY => ControlFile,
     304            0 :             pageserver_api::key::CHECKPOINT_KEY => Checkpoint,
     305            0 :             pageserver_api::key::AUX_FILES_KEY => AuxFilesV1,
     306            0 :             _ if slru_dir_kind.is_some() => SlruDir(slru_dir_kind.unwrap()),
     307            0 :             _ if key.field1 == 0 && key.field4 == 0 && key.field5 == 0 && key.field6 == 0 => {
     308            0 :                 RelMap([key.field2, key.field3].into())
     309              :             }
     310            0 :             _ if key.field1 == 0 && key.field4 == 0 && key.field5 == 0 && key.field6 == 1 => {
     311            0 :                 RelDir([key.field2, key.field3].into())
     312              :             }
     313            0 :             _ if key.is_metadata_key() => RecognizedKeyKind::AuxFileV2(
     314            0 :                 AuxFileV2::new(key).ok_or_else(|| utils::Hex(key.to_i128().to_be_bytes())),
     315            0 :             ),
     316            0 :             _ => return None,
     317              :         })
     318            0 :     }
     319              : }
     320              : 
     321              : impl AuxFileV2 {
     322            7 :     fn new(key: Key) -> Option<AuxFileV2> {
     323              :         const EMPTY_HASH: [u8; 13] = {
     324              :             let mut out = [0u8; 13];
     325              :             let hash = pageserver::aux_file::fnv_hash(b"").to_be_bytes();
     326              :             let mut i = 3;
     327              :             while i < 16 {
     328              :                 out[i - 3] = hash[i];
     329              :                 i += 1;
     330              :             }
     331              :             out
     332              :         };
     333              : 
     334            7 :         let bytes = key.to_i128().to_be_bytes();
     335            7 :         let hash = utils::Hex(<[u8; 13]>::try_from(&bytes[3..]).unwrap());
     336            7 : 
     337            7 :         assert_eq!(EMPTY_HASH.len(), hash.0.len());
     338              : 
     339              :         // TODO: we could probably find the preimages for the hashes
     340              : 
     341            7 :         Some(match (bytes[1], bytes[2]) {
     342            1 :             (1, 1) => AuxFileV2::Recognized("pg_logical/mappings/", hash),
     343            1 :             (1, 2) => AuxFileV2::Recognized("pg_logical/snapshots/", hash),
     344            1 :             (1, 3) if hash.0 == EMPTY_HASH => {
     345            1 :                 AuxFileV2::Recognized("pg_logical/replorigin_checkpoint", hash)
     346              :             }
     347            1 :             (2, 1) => AuxFileV2::Recognized("pg_replslot/", hash),
     348            1 :             (1, 0xff) => AuxFileV2::OtherWithPrefix("pg_logical/", hash),
     349            1 :             (0xff, 0xff) => AuxFileV2::Other(hash),
     350            1 :             _ => return None,
     351              :         })
     352            7 :     }
     353              : }
     354              : 
     355              : /// Prefix of RelTag, currently only known use cases are the two item versions.
     356              : ///
     357              : /// Renders like a reltag with `/`, nothing else.
     358              : struct RelTagish<const N: usize>([u32; N]);
     359              : 
     360              : impl<const N: usize> From<[u32; N]> for RelTagish<N> {
     361            0 :     fn from(val: [u32; N]) -> Self {
     362            0 :         RelTagish(val)
     363            0 :     }
     364              : }
     365              : 
     366              : impl<const N: usize> std::fmt::Debug for RelTagish<N> {
     367            0 :     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
     368              :         use std::fmt::Write as _;
     369            0 :         let mut first = true;
     370            0 :         self.0.iter().try_for_each(|x| {
     371            0 :             if !first {
     372            0 :                 f.write_char('/')?;
     373            0 :             }
     374            0 :             first = false;
     375            0 :             write!(f, "{}", x)
     376            0 :         })
     377            0 :     }
     378              : }
     379              : 
     380              : #[cfg(test)]
     381              : mod tests {
     382              :     use pageserver::aux_file::encode_aux_file_key;
     383              : 
     384              :     use super::*;
     385              : 
     386              :     #[test]
     387            1 :     fn hex_is_key_material() {
     388            1 :         let m = KeyMaterial::try_from(&["000000067F0000400200DF927900FFFFFFFF"][..]).unwrap();
     389            1 :         assert!(matches!(m, KeyMaterial::Hex(_)), "{m:?}");
     390            1 :     }
     391              : 
     392              :     #[test]
     393            1 :     fn single_positional_spanalike_is_key_material() {
     394            1 :         // why is this needed? if you are checking many, then copypaste starts to appeal
     395            1 :         let strings = [
     396            1 :             (line!(), "2024-05-15T15:33:49.873906Z ERROR page_service_conn_main{peer_addr=A:B}:process_query{tenant_id=C timeline_id=D}:handle_pagerequests:handle_get_page_at_lsn_request{rel=1663/208101/2620_fsm blkno=2 req_lsn=0/238D98C8}: error reading relation or page version: Read error: could not find data for key 000000067F00032CE5000000000000000001 (shard ShardNumber(0)) at LSN 0/1D0A16C1, request LSN 0/238D98C8, ancestor 0/0"),
     397            1 :             (line!(), "rel=1663/208101/2620_fsm blkno=2"),
     398            1 :             (line!(), "rel=1663/208101/2620.1 blkno=2"),
     399            1 :         ];
     400            1 : 
     401            1 :         let mut first: Option<Key> = None;
     402              : 
     403            4 :         for (line, example) in strings {
     404            3 :             let m = KeyMaterial::try_from(&[example][..])
     405            3 :                 .unwrap_or_else(|e| panic!("failed to parse example from line {line}: {e:?}"));
     406            3 :             let key = Key::from(m);
     407            3 :             if let Some(first) = first {
     408            2 :                 assert_eq!(first, key);
     409            1 :             } else {
     410            1 :                 first = Some(key);
     411            1 :             }
     412              :         }
     413              : 
     414              :         // not supporting this is rather accidential, but I think the input parsing is lenient
     415              :         // enough already
     416            1 :         KeyMaterial::try_from(&["1663/208101/2620_fsm 2"][..]).unwrap_err();
     417            1 :     }
     418              : 
     419              :     #[test]
     420            1 :     fn multiple_spanlike_args() {
     421            1 :         let strings = [
     422            1 :             (line!(), &["process_query{tenant_id=C", "timeline_id=D}:handle_pagerequests:handle_get_page_at_lsn_request{rel=1663/208101/2620_fsm", "blkno=2", "req_lsn=0/238D98C8}"][..]),
     423            1 :             (line!(), &["rel=1663/208101/2620_fsm", "blkno=2"][..]),
     424            1 :             (line!(), &["1663/208101/2620_fsm", "2"][..]),
     425            1 :         ];
     426            1 : 
     427            1 :         let mut first: Option<Key> = None;
     428              : 
     429            4 :         for (line, example) in strings {
     430            3 :             let m = KeyMaterial::try_from(example)
     431            3 :                 .unwrap_or_else(|e| panic!("failed to parse example from line {line}: {e:?}"));
     432            3 :             let key = Key::from(m);
     433            3 :             if let Some(first) = first {
     434            2 :                 assert_eq!(first, key);
     435            1 :             } else {
     436            1 :                 first = Some(key);
     437            1 :             }
     438              :         }
     439            1 :     }
     440              :     #[test]
     441            1 :     fn recognized_auxfiles() {
     442              :         use AuxFileV2::*;
     443              : 
     444            1 :         let empty = [
     445            1 :             0x2e, 0x07, 0xbb, 0x01, 0x42, 0x62, 0xb8, 0x21, 0x75, 0x62, 0x95, 0xc5, 0x8d,
     446            1 :         ];
     447            1 :         let foobar = [
     448            1 :             0x62, 0x79, 0x3c, 0x64, 0xbf, 0x6f, 0x0d, 0x35, 0x97, 0xba, 0x44, 0x6f, 0x18,
     449            1 :         ];
     450            1 : 
     451            1 :         #[rustfmt::skip]
     452            1 :         let examples = [
     453            1 :             (line!(), "pg_logical/mappings/foobar", Recognized("pg_logical/mappings/", utils::Hex(foobar))),
     454            1 :             (line!(), "pg_logical/snapshots/foobar", Recognized("pg_logical/snapshots/", utils::Hex(foobar))),
     455            1 :             (line!(), "pg_logical/replorigin_checkpoint", Recognized("pg_logical/replorigin_checkpoint", utils::Hex(empty))),
     456            1 :             (line!(), "pg_logical/foobar", OtherWithPrefix("pg_logical/", utils::Hex(foobar))),
     457            1 :             (line!(), "pg_replslot/foobar", Recognized("pg_replslot/", utils::Hex(foobar))),
     458            1 :             (line!(), "foobar", Other(utils::Hex(foobar))),
     459            1 :         ];
     460              : 
     461            7 :         for (line, path, expected) in examples {
     462            6 :             let key = encode_aux_file_key(path);
     463            6 :             let recognized =
     464            6 :                 AuxFileV2::new(key).unwrap_or_else(|| panic!("line {line} example failed"));
     465            6 : 
     466            6 :             assert_eq!(recognized, expected);
     467              :         }
     468              : 
     469            1 :         assert_eq!(
     470            1 :             AuxFileV2::new(Key::from_hex("600000102000000000000000000000000000").unwrap()),
     471              :             None,
     472            0 :             "example key has one too few 0 after 6 before 1"
     473              :         );
     474            1 :     }
     475              : }
        

Generated by: LCOV version 2.1-beta