将fetch pp cache改成使用vcir结构,跑通apnic全量同步

This commit is contained in:
2026-03-13 14:45:41 +08:00
parent e3339533b8
commit cf764c35bb
53 changed files with 8931 additions and 2201 deletions

View File

@@ -1,21 +1,36 @@
use std::collections::BTreeMap;
use std::path::PathBuf;
use rocksdb::{ColumnFamilyDescriptor, DB, IteratorMode, Options};
use rocksdb::{DB, IteratorMode, Options};
use rpki::storage::{
ALL_COLUMN_FAMILY_NAMES, CF_AUDIT_RULE_INDEX, CF_RAW_BY_HASH, CF_RAW_OBJECTS,
CF_REPOSITORY_VIEW, CF_RRDP_OBJECT_INDEX, CF_RRDP_SOURCE, CF_RRDP_SOURCE_MEMBER, CF_RRDP_STATE,
CF_RRDP_URI_OWNER, CF_VCIR, column_family_descriptors,
};
const CF_RAW_OBJECTS: &str = "raw_objects";
const CF_FETCH_CACHE_PP: &str = "fetch_cache_pp";
const CF_RRDP_STATE: &str = "rrdp_state";
const CF_RRDP_OBJECT_INDEX: &str = "rrdp_object_index";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum DbStatsMode {
Estimate,
Exact,
}
fn enable_blobdb_if_supported(opts: &mut Options) {
// Keep this in sync with `rpki::storage`:
// blob files are CF-level options; readers should open CFs with blob enabled too.
#[allow(dead_code)]
fn _set(opts: &mut Options) {
opts.set_enable_blob_files(true);
opts.set_min_blob_size(1024);
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
enum CfGroup {
CurrentRepositoryView,
CurrentValidationState,
CurrentRrdpState,
LegacyCompatibility,
}
impl CfGroup {
fn as_str(self) -> &'static str {
match self {
Self::CurrentRepositoryView => "current_repository_view",
Self::CurrentValidationState => "current_validation_state",
Self::CurrentRrdpState => "current_rrdp_state",
Self::LegacyCompatibility => "legacy_compatibility",
}
}
_set(opts);
}
fn usage() -> String {
@@ -29,21 +44,16 @@ Options:
--db <path> RocksDB directory
--exact Iterate to count keys (slower; default uses RocksDB estimates)
--help Show this help
Output groups:
- current_repository_view: repository_view + raw_by_hash
- current_validation_state: vcir + audit_rule_index
- current_rrdp_state: rrdp_state + rrdp_source + rrdp_source_member + rrdp_uri_owner
- legacy_compatibility: raw_objects + rrdp_object_index
"
)
}
fn cf_descriptors() -> Vec<ColumnFamilyDescriptor> {
let mut cf_opts = Options::default();
enable_blobdb_if_supported(&mut cf_opts);
vec![
ColumnFamilyDescriptor::new(CF_RAW_OBJECTS, cf_opts.clone()),
ColumnFamilyDescriptor::new(CF_FETCH_CACHE_PP, cf_opts.clone()),
ColumnFamilyDescriptor::new(CF_RRDP_STATE, cf_opts.clone()),
ColumnFamilyDescriptor::new(CF_RRDP_OBJECT_INDEX, cf_opts),
]
}
fn estimate_keys(db: &DB, cf_name: &str) -> Result<Option<u64>, Box<dyn std::error::Error>> {
let cf = db
.cf_handle(cf_name)
@@ -64,6 +74,35 @@ fn exact_keys(db: &DB, cf_name: &str) -> Result<u64, Box<dyn std::error::Error>>
Ok(count)
}
fn cf_group(cf_name: &str) -> CfGroup {
match cf_name {
CF_REPOSITORY_VIEW | CF_RAW_BY_HASH => CfGroup::CurrentRepositoryView,
CF_VCIR | CF_AUDIT_RULE_INDEX => CfGroup::CurrentValidationState,
CF_RRDP_STATE | CF_RRDP_SOURCE | CF_RRDP_SOURCE_MEMBER | CF_RRDP_URI_OWNER => {
CfGroup::CurrentRrdpState
}
CF_RAW_OBJECTS | CF_RRDP_OBJECT_INDEX => CfGroup::LegacyCompatibility,
_ => CfGroup::LegacyCompatibility,
}
}
fn summarize_counts<'a>(
counts: impl IntoIterator<Item = (&'a str, u64)>,
) -> BTreeMap<CfGroup, u64> {
let mut grouped = BTreeMap::new();
for (cf_name, count) in counts {
*grouped.entry(cf_group(cf_name)).or_insert(0) += count;
}
grouped
}
fn mode_label(mode: DbStatsMode) -> &'static str {
match mode {
DbStatsMode::Estimate => "estimate",
DbStatsMode::Exact => "exact",
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let argv: Vec<String> = std::env::args().collect();
if argv.iter().any(|a| a == "--help" || a == "-h") {
@@ -72,7 +111,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
let mut db_path: Option<PathBuf> = None;
let mut exact = false;
let mut mode = DbStatsMode::Estimate;
let mut i = 1usize;
while i < argv.len() {
match argv[i].as_str() {
@@ -81,7 +120,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let v = argv.get(i).ok_or("--db requires a value")?;
db_path = Some(PathBuf::from(v));
}
"--exact" => exact = true,
"--exact" => mode = DbStatsMode::Exact,
other => return Err(format!("unknown argument: {other}\n\n{}", usage()).into()),
}
i += 1;
@@ -93,33 +132,75 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
opts.create_if_missing(false);
opts.create_missing_column_families(false);
let db = DB::open_cf_descriptors(&opts, &db_path, cf_descriptors())?;
let cf_names = [
CF_RAW_OBJECTS,
CF_FETCH_CACHE_PP,
CF_RRDP_STATE,
CF_RRDP_OBJECT_INDEX,
];
let db = DB::open_cf_descriptors(&opts, &db_path, column_family_descriptors())?;
println!("db={}", db_path.display());
println!("mode={}", if exact { "exact" } else { "estimate" });
println!("mode={}", mode_label(mode));
let mut per_cf = Vec::with_capacity(ALL_COLUMN_FAMILY_NAMES.len());
let mut total: u64 = 0;
for name in cf_names {
let n = if exact {
exact_keys(&db, name)?
} else {
estimate_keys(&db, name)?.unwrap_or(0)
for &name in ALL_COLUMN_FAMILY_NAMES {
let n = match mode {
DbStatsMode::Exact => exact_keys(&db, name)?,
DbStatsMode::Estimate => estimate_keys(&db, name)?.unwrap_or(0),
};
total = total.saturating_add(n);
per_cf.push((name, n));
println!("{name}={n}");
}
println!("total={total}");
// Also print # of SST files (useful sanity signal).
for (group, count) in summarize_counts(per_cf.iter().copied()) {
println!("group_{}={count}", group.as_str());
}
let live = db.live_files()?;
println!("sst_files={}", live.len());
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cf_group_classifies_current_and_legacy_keyspaces() {
assert_eq!(cf_group(CF_REPOSITORY_VIEW), CfGroup::CurrentRepositoryView);
assert_eq!(cf_group(CF_RAW_BY_HASH), CfGroup::CurrentRepositoryView);
assert_eq!(cf_group(CF_VCIR), CfGroup::CurrentValidationState);
assert_eq!(
cf_group(CF_AUDIT_RULE_INDEX),
CfGroup::CurrentValidationState
);
assert_eq!(cf_group(CF_RRDP_SOURCE), CfGroup::CurrentRrdpState);
assert_eq!(cf_group(CF_RRDP_URI_OWNER), CfGroup::CurrentRrdpState);
assert_eq!(cf_group(CF_RAW_OBJECTS), CfGroup::LegacyCompatibility);
}
#[test]
fn summarize_counts_accumulates_by_group() {
let grouped = summarize_counts([
(CF_REPOSITORY_VIEW, 5),
(CF_RAW_BY_HASH, 7),
(CF_VCIR, 11),
(CF_AUDIT_RULE_INDEX, 13),
(CF_RRDP_STATE, 17),
(CF_RRDP_SOURCE_MEMBER, 19),
(CF_RRDP_OBJECT_INDEX, 29),
]);
assert_eq!(grouped.get(&CfGroup::CurrentRepositoryView), Some(&12));
assert_eq!(grouped.get(&CfGroup::CurrentValidationState), Some(&24));
assert_eq!(grouped.get(&CfGroup::CurrentRrdpState), Some(&36));
assert_eq!(grouped.get(&CfGroup::LegacyCompatibility), Some(&29));
}
#[test]
fn usage_mentions_grouped_output_and_exact_mode() {
let text = usage();
assert!(text.contains("--exact"), "{text}");
assert!(text.contains("current_validation_state"), "{text}");
assert!(text.contains("legacy_compatibility"), "{text}");
}
}

View File

@@ -1,16 +1,40 @@
use std::path::PathBuf;
use rocksdb::{ColumnFamilyDescriptor, DB, IteratorMode, Options};
use rocksdb::{DB, IteratorMode, Options};
use rpki::storage::{
CF_RRDP_SOURCE, CF_RRDP_SOURCE_MEMBER, CF_RRDP_STATE, CF_RRDP_URI_OWNER,
RrdpSourceMemberRecord, RrdpSourceRecord, RrdpUriOwnerRecord, column_family_descriptors,
};
use rpki::sync::rrdp::RrdpState;
fn enable_blobdb_if_supported(opts: &mut Options) {
// Keep this in sync with `rpki::storage`:
// blob files are CF-level options; readers should open CFs with blob enabled too.
#[allow(dead_code)]
fn _set(opts: &mut Options) {
opts.set_enable_blob_files(true);
opts.set_min_blob_size(1024);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum DumpView {
LegacyState,
Source,
Members,
Owners,
All,
}
impl DumpView {
fn parse(value: &str) -> Result<Self, String> {
match value {
"legacy-state" => Ok(Self::LegacyState),
"source" => Ok(Self::Source),
"members" => Ok(Self::Members),
"owners" => Ok(Self::Owners),
"all" => Ok(Self::All),
other => Err(format!(
"invalid --view: {other} (expected one of: legacy-state, source, members, owners, all)"
)),
}
}
_set(opts);
}
#[derive(Debug)]
struct DumpArgs {
db_path: PathBuf,
view: DumpView,
}
fn usage() -> String {
@@ -18,23 +42,23 @@ fn usage() -> String {
format!(
"\
Usage:
{bin} --db <path>
{bin} --db <path> [--view <legacy-state|source|members|owners|all>]
Options:
--db <path> RocksDB directory
--view <name> Dump one RRDP view; default is all
--help Show this help
"
)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let argv: Vec<String> = std::env::args().collect();
fn parse_args(argv: &[String]) -> Result<DumpArgs, String> {
if argv.iter().any(|a| a == "--help" || a == "-h") {
print!("{}", usage());
return Ok(());
return Err(usage());
}
let mut db_path: Option<PathBuf> = None;
let mut view = DumpView::All;
let mut i = 1usize;
while i < argv.len() {
match argv[i].as_str() {
@@ -43,43 +67,280 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let v = argv.get(i).ok_or("--db requires a value")?;
db_path = Some(PathBuf::from(v));
}
other => return Err(format!("unknown argument: {other}\n\n{}", usage()).into()),
"--view" => {
i += 1;
let v = argv.get(i).ok_or("--view requires a value")?;
view = DumpView::parse(v)?;
}
other => return Err(format!("unknown argument: {other}\n\n{}", usage())),
}
i += 1;
}
let db_path = db_path.ok_or_else(|| format!("--db is required\n\n{}", usage()))?;
Ok(DumpArgs {
db_path: db_path.ok_or_else(|| format!("--db is required\n\n{}", usage()))?,
view,
})
}
fn open_db(path: &std::path::Path) -> Result<DB, Box<dyn std::error::Error>> {
let mut opts = Options::default();
opts.create_if_missing(false);
opts.create_missing_column_families(false);
Ok(DB::open_cf_descriptors(
&opts,
path,
column_family_descriptors(),
)?)
}
// Open only the column families we need.
let mut cf_opts = Options::default();
enable_blobdb_if_supported(&mut cf_opts);
let cfs = vec![
ColumnFamilyDescriptor::new("raw_objects", cf_opts.clone()),
ColumnFamilyDescriptor::new("fetch_cache_pp", cf_opts.clone()),
ColumnFamilyDescriptor::new("rrdp_state", cf_opts.clone()),
ColumnFamilyDescriptor::new("rrdp_object_index", cf_opts),
];
let db = DB::open_cf_descriptors(&opts, &db_path, cfs)?;
fn collect_legacy_state(db: &DB) -> Result<Vec<(String, RrdpState)>, Box<dyn std::error::Error>> {
let cf = db
.cf_handle("rrdp_state")
.cf_handle(CF_RRDP_STATE)
.ok_or("missing column family: rrdp_state")?;
let mut out: Vec<(String, u64, String)> = Vec::new();
let mut out = Vec::new();
for res in db.iterator_cf(cf, IteratorMode::Start) {
let (k, v) = res?;
let k = String::from_utf8_lossy(&k).to_string();
let st = rpki::sync::rrdp::RrdpState::decode(&v)
.map_err(|e| format!("decode rrdp_state failed for {k}: {e}"))?;
out.push((k, st.serial, st.session_id));
let notify_uri = String::from_utf8_lossy(&k).to_string();
let state = RrdpState::decode(&v)
.map_err(|e| format!("decode rrdp_state failed for {notify_uri}: {e}"))?;
out.push((notify_uri, state));
}
out.sort_by(|a, b| a.0.cmp(&b.0));
for (k, serial, session) in out {
println!("{k}\t{serial}\t{session}");
Ok(out)
}
fn collect_source_records(db: &DB) -> Result<Vec<RrdpSourceRecord>, Box<dyn std::error::Error>> {
let cf = db
.cf_handle(CF_RRDP_SOURCE)
.ok_or("missing column family: rrdp_source")?;
let mut out = Vec::new();
for res in db.iterator_cf(cf, IteratorMode::Start) {
let (_k, v) = res?;
let record: RrdpSourceRecord = serde_cbor::from_slice(&v)?;
record.validate_internal()?;
out.push(record);
}
out.sort_by(|a, b| a.notify_uri.cmp(&b.notify_uri));
Ok(out)
}
fn collect_source_member_records(
db: &DB,
) -> Result<Vec<RrdpSourceMemberRecord>, Box<dyn std::error::Error>> {
let cf = db
.cf_handle(CF_RRDP_SOURCE_MEMBER)
.ok_or("missing column family: rrdp_source_member")?;
let mut out = Vec::new();
for res in db.iterator_cf(cf, IteratorMode::Start) {
let (_k, v) = res?;
let record: RrdpSourceMemberRecord = serde_cbor::from_slice(&v)?;
record.validate_internal()?;
out.push(record);
}
out.sort_by(|a, b| {
a.notify_uri
.cmp(&b.notify_uri)
.then(a.rsync_uri.cmp(&b.rsync_uri))
});
Ok(out)
}
fn collect_uri_owner_records(
db: &DB,
) -> Result<Vec<RrdpUriOwnerRecord>, Box<dyn std::error::Error>> {
let cf = db
.cf_handle(CF_RRDP_URI_OWNER)
.ok_or("missing column family: rrdp_uri_owner")?;
let mut out = Vec::new();
for res in db.iterator_cf(cf, IteratorMode::Start) {
let (_k, v) = res?;
let record: RrdpUriOwnerRecord = serde_cbor::from_slice(&v)?;
record.validate_internal()?;
out.push(record);
}
out.sort_by(|a, b| a.rsync_uri.cmp(&b.rsync_uri));
Ok(out)
}
fn print_legacy_state(entries: &[(String, RrdpState)]) {
println!("[legacy-state]");
println!("notify_uri\tserial\tsession_id");
for (notify_uri, state) in entries {
println!("{notify_uri}\t{}\t{}", state.serial, state.session_id);
}
}
fn print_source_records(entries: &[RrdpSourceRecord]) {
println!("[source]");
println!("notify_uri\tlast_serial\tlast_session_id\tsync_state\tlast_snapshot_uri\tlast_error");
for record in entries {
println!(
"{}\t{}\t{}\t{:?}\t{}\t{}",
record.notify_uri,
record
.last_serial
.map(|v| v.to_string())
.unwrap_or_else(|| "-".to_string()),
record.last_session_id.as_deref().unwrap_or("-"),
record.sync_state,
record.last_snapshot_uri.as_deref().unwrap_or("-"),
record.last_error.as_deref().unwrap_or("-"),
);
}
}
fn print_source_member_records(entries: &[RrdpSourceMemberRecord]) {
println!("[members]");
println!("notify_uri\trsync_uri\tpresent\thash\tsession_id\tserial");
for record in entries {
println!(
"{}\t{}\t{}\t{}\t{}\t{}",
record.notify_uri,
record.rsync_uri,
record.present,
record.current_hash.as_deref().unwrap_or("-"),
record.last_confirmed_session_id,
record.last_confirmed_serial,
);
}
}
fn print_uri_owner_records(entries: &[RrdpUriOwnerRecord]) {
println!("[owners]");
println!("rsync_uri\tnotify_uri\towner_state\thash\tsession_id\tserial");
for record in entries {
println!(
"{}\t{}\t{:?}\t{}\t{}\t{}",
record.rsync_uri,
record.notify_uri,
record.owner_state,
record.current_hash.as_deref().unwrap_or("-"),
record.last_confirmed_session_id,
record.last_confirmed_serial,
);
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let argv: Vec<String> = std::env::args().collect();
let args = parse_args(&argv).map_err(|e| -> Box<dyn std::error::Error> { e.into() })?;
let db = open_db(&args.db_path)?;
match args.view {
DumpView::LegacyState => print_legacy_state(&collect_legacy_state(&db)?),
DumpView::Source => print_source_records(&collect_source_records(&db)?),
DumpView::Members => print_source_member_records(&collect_source_member_records(&db)?),
DumpView::Owners => print_uri_owner_records(&collect_uri_owner_records(&db)?),
DumpView::All => {
print_legacy_state(&collect_legacy_state(&db)?);
print_source_records(&collect_source_records(&db)?);
print_source_member_records(&collect_source_member_records(&db)?);
print_uri_owner_records(&collect_uri_owner_records(&db)?);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use rpki::storage::{PackTime, RocksStore, RrdpSourceSyncState, RrdpUriOwnerState};
#[test]
fn parse_args_accepts_view_and_db() {
let args = parse_args(&[
"rrdp_state_dump".to_string(),
"--db".to_string(),
"db".to_string(),
"--view".to_string(),
"owners".to_string(),
])
.expect("parse args");
assert_eq!(args.db_path, PathBuf::from("db"));
assert_eq!(args.view, DumpView::Owners);
}
#[test]
fn parse_args_rejects_invalid_view() {
let err = parse_args(&[
"rrdp_state_dump".to_string(),
"--db".to_string(),
"db".to_string(),
"--view".to_string(),
"nope".to_string(),
])
.unwrap_err();
assert!(err.contains("invalid --view"), "{err}");
}
#[test]
fn collect_rrdp_views_reads_legacy_and_current_records() {
let dir = tempfile::tempdir().expect("tempdir");
let store = RocksStore::open(dir.path()).expect("open store");
let legacy_state = RrdpState {
session_id: "session-1".to_string(),
serial: 42,
};
store
.put_rrdp_state(
"https://example.test/notify.xml",
&legacy_state.encode().expect("encode legacy state"),
)
.expect("put legacy state");
store
.put_rrdp_source_record(&RrdpSourceRecord {
notify_uri: "https://example.test/notify.xml".to_string(),
last_session_id: Some("session-1".to_string()),
last_serial: Some(42),
first_seen_at: PackTime::from_utc_offset_datetime(time::OffsetDateTime::now_utc()),
last_seen_at: PackTime::from_utc_offset_datetime(time::OffsetDateTime::now_utc()),
last_sync_at: None,
sync_state: RrdpSourceSyncState::DeltaReady,
last_snapshot_uri: Some("https://example.test/snapshot.xml".to_string()),
last_snapshot_hash: None,
last_error: None,
})
.expect("put source record");
store
.put_rrdp_source_member_record(&RrdpSourceMemberRecord {
notify_uri: "https://example.test/notify.xml".to_string(),
rsync_uri: "rsync://example.test/repo/a.roa".to_string(),
current_hash: Some("11".repeat(32)),
object_type: Some("roa".to_string()),
present: true,
last_confirmed_session_id: "session-1".to_string(),
last_confirmed_serial: 42,
last_changed_at: PackTime::from_utc_offset_datetime(time::OffsetDateTime::now_utc()),
})
.expect("put member record");
store
.put_rrdp_uri_owner_record(&RrdpUriOwnerRecord {
rsync_uri: "rsync://example.test/repo/a.roa".to_string(),
notify_uri: "https://example.test/notify.xml".to_string(),
current_hash: Some("11".repeat(32)),
last_confirmed_session_id: "session-1".to_string(),
last_confirmed_serial: 42,
last_changed_at: PackTime::from_utc_offset_datetime(time::OffsetDateTime::now_utc()),
owner_state: RrdpUriOwnerState::Active,
})
.expect("put owner record");
drop(store);
let db = open_db(dir.path()).expect("open db");
let legacy = collect_legacy_state(&db).expect("legacy dump");
let sources = collect_source_records(&db).expect("source dump");
let members = collect_source_member_records(&db).expect("members dump");
let owners = collect_uri_owner_records(&db).expect("owners dump");
assert_eq!(legacy.len(), 1);
assert_eq!(legacy[0].0, "https://example.test/notify.xml");
assert_eq!(legacy[0].1.serial, 42);
assert_eq!(sources.len(), 1);
assert_eq!(sources[0].sync_state, RrdpSourceSyncState::DeltaReady);
assert_eq!(members.len(), 1);
assert_eq!(members[0].rsync_uri, "rsync://example.test/repo/a.roa");
assert_eq!(owners.len(), 1);
assert_eq!(owners[0].owner_state, RrdpUriOwnerState::Active);
}
}