20260617 发布点级验证缓存复用

This commit is contained in:
2026-06-17 23:49:53 +08:00
parent 68006467f7
commit ad2a25aede
18 changed files with 1841 additions and 14 deletions

View File

@@ -4,10 +4,11 @@ use crate::data_model::roa::RoaAfi;
use crate::fetch::rsync::LocalDirRsyncFetcher;
use crate::fetch::rsync::{RsyncFetchError, RsyncFetcher};
use crate::storage::{
PackFile, PackTime, RawByHashEntry, RocksStore, ValidatedCaInstanceResult,
ValidatedManifestMeta, VcirArtifactKind, VcirArtifactRole, VcirArtifactValidationStatus,
VcirAuditSummary, VcirChildEntry, VcirInstanceGate, VcirLocalOutput, VcirLocalOutputPayload,
VcirOutputType, VcirRelatedArtifact, VcirSourceObjectType, VcirSummary,
PackFile, PackTime, PublicationPointCacheProjection, RawByHashEntry, RepositoryViewEntry,
RepositoryViewState, RocksStore, ValidatedCaInstanceResult, ValidatedManifestMeta,
VcirArtifactKind, VcirArtifactRole, VcirArtifactValidationStatus, VcirAuditSummary,
VcirChildEntry, VcirInstanceGate, VcirLocalOutput, VcirLocalOutputPayload, VcirOutputType,
VcirRelatedArtifact, VcirSourceObjectType, VcirSummary,
};
use crate::sync::rrdp::Fetcher;
use crate::validation::publication_point::PublicationPointSnapshot;
@@ -69,6 +70,8 @@ fn sample_runner_with_ccr_accumulator<'a>(
ccr_accumulator: Some(Mutex::new(CcrAccumulator::new(Vec::new()))),
persist_vcir: true,
enable_roa_validation_cache: false,
publication_point_cache_observe_only: false,
enable_publication_point_validation_cache: false,
}
}
@@ -960,6 +963,8 @@ fn finalize_fresh_publication_point_releases_local_outputs_cache_after_persist()
ccr_accumulator: None,
persist_vcir: true,
enable_roa_validation_cache: false,
publication_point_cache_observe_only: false,
enable_publication_point_validation_cache: false,
};
let ca = CaInstanceHandle {
depth: 0,
@@ -1062,6 +1067,7 @@ fn persist_vcir_for_fresh_result_stores_vcir_and_replay_meta_for_real_snapshot()
let mut objects = objects;
persist_vcir_for_fresh_result_with_timing(
&store,
&Policy::default(),
&ca,
&pack,
&mut objects,
@@ -1069,6 +1075,7 @@ fn persist_vcir_for_fresh_result_stores_vcir_and_replay_meta_for_real_snapshot()
&[],
&[],
validation_time,
false,
)
.map(|_timing| ())
.expect("persist vcir for fresh result");
@@ -1522,6 +1529,8 @@ fn runner_offline_rsync_fixture_produces_pack_and_warnings() {
ccr_accumulator: None,
persist_vcir: true,
enable_roa_validation_cache: false,
publication_point_cache_observe_only: false,
enable_publication_point_validation_cache: false,
};
// For this fixture-driven smoke, we provide the correct issuer CA certificate (the CA for
@@ -1653,6 +1662,8 @@ fn runner_roa_validation_cache_reuses_vcir_outputs_on_second_fixture_run() {
ccr_accumulator: None,
persist_vcir: true,
enable_roa_validation_cache: false,
publication_point_cache_observe_only: false,
enable_publication_point_validation_cache: false,
};
let first = first_runner
.run_publication_point(&handle)
@@ -1681,6 +1692,8 @@ fn runner_roa_validation_cache_reuses_vcir_outputs_on_second_fixture_run() {
ccr_accumulator: None,
persist_vcir: true,
enable_roa_validation_cache: true,
publication_point_cache_observe_only: false,
enable_publication_point_validation_cache: false,
};
let second = second_runner
.run_publication_point(&handle)
@@ -1702,6 +1715,456 @@ fn runner_roa_validation_cache_reuses_vcir_outputs_on_second_fixture_run() {
assert_eq!(second.objects.roa_cache_stats.fresh_roas, 0);
}
#[test]
fn runner_publication_point_cache_observe_and_reuse_path() {
let fixture_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0");
assert!(fixture_dir.is_dir(), "fixture directory must exist");
let rsync_base_uri = "rsync://rpki.cernet.net/repo/cernet/0/".to_string();
let manifest_file = "05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft";
let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}");
let fixture_manifest_bytes =
std::fs::read(fixture_dir.join(manifest_file)).expect("read manifest fixture");
let fixture_manifest =
crate::data_model::manifest::ManifestObject::decode_der(&fixture_manifest_bytes)
.expect("decode manifest fixture");
let validation_time = fixture_manifest.manifest.this_update + time::Duration::seconds(60);
let store_dir = tempfile::tempdir().expect("store dir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
let policy = Policy {
sync_preference: crate::policy::SyncPreference::RsyncOnly,
..Policy::default()
};
let issuer_ca_der = std::fs::read(
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
),
)
.expect("read issuer ca fixture");
let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer ca");
let handle = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate_der: issuer_ca_der,
ca_certificate_rsync_uri: Some("rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string()),
effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
rsync_base_uri: rsync_base_uri.clone(),
manifest_rsync_uri: manifest_rsync_uri.clone(),
publication_point_rsync_uri: rsync_base_uri,
rrdp_notification_uri: None,
};
let first_runner = Rpkiv1PublicationPointRunner {
store: &store,
policy: &policy,
http_fetcher: &NeverHttpFetcher,
rsync_fetcher: &LocalDirRsyncFetcher::new(&fixture_dir),
validation_time,
timing: None,
download_log: None,
replay_archive_index: None,
replay_delta_index: None,
rrdp_dedup: false,
rrdp_repo_cache: Mutex::new(HashMap::new()),
rsync_dedup: false,
rsync_repo_cache: Mutex::new(HashMap::new()),
current_repo_index: None,
repo_sync_runtime: None,
parallel_phase2_config: None,
parallel_roa_worker_pool: None,
ccr_accumulator: None,
persist_vcir: true,
enable_roa_validation_cache: false,
publication_point_cache_observe_only: true,
enable_publication_point_validation_cache: false,
};
let first = first_runner
.run_publication_point(&handle)
.expect("first fresh run");
assert_eq!(first.source, PublicationPointSource::Fresh);
assert!(
store
.get_publication_point_cache_projection(&manifest_rsync_uri)
.expect("load publication-point projection")
.is_some()
);
let timing = crate::analysis::timing::TimingHandle::new(crate::analysis::timing::TimingMeta {
recorded_at_utc_rfc3339: "2026-01-01T00:00:00Z".to_string(),
validation_time_utc_rfc3339: "2026-01-01T00:00:00Z".to_string(),
tal_url: None,
db_path: None,
});
let observe_runner = Rpkiv1PublicationPointRunner {
store: &store,
policy: &policy,
http_fetcher: &NeverHttpFetcher,
rsync_fetcher: &LocalDirRsyncFetcher::new(&fixture_dir),
validation_time,
timing: Some(timing.clone()),
download_log: None,
replay_archive_index: None,
replay_delta_index: None,
rrdp_dedup: false,
rrdp_repo_cache: Mutex::new(HashMap::new()),
rsync_dedup: false,
rsync_repo_cache: Mutex::new(HashMap::new()),
current_repo_index: None,
repo_sync_runtime: None,
parallel_phase2_config: None,
parallel_roa_worker_pool: None,
ccr_accumulator: None,
persist_vcir: true,
enable_roa_validation_cache: false,
publication_point_cache_observe_only: true,
enable_publication_point_validation_cache: false,
};
let observed = observe_runner
.run_publication_point(&handle)
.expect("observe-only run");
assert_eq!(observed.source, PublicationPointSource::Fresh);
assert_eq!(observed.objects.vrps, first.objects.vrps);
assert_eq!(
timing
.counts_snapshot()
.get("publication_point_cache_theoretical_hits")
.copied(),
Some(1)
);
let cache_runner = Rpkiv1PublicationPointRunner {
store: &store,
policy: &policy,
http_fetcher: &NeverHttpFetcher,
rsync_fetcher: &LocalDirRsyncFetcher::new(&fixture_dir),
validation_time,
timing: None,
download_log: None,
replay_archive_index: None,
replay_delta_index: None,
rrdp_dedup: false,
rrdp_repo_cache: Mutex::new(HashMap::new()),
rsync_dedup: false,
rsync_repo_cache: Mutex::new(HashMap::new()),
current_repo_index: None,
repo_sync_runtime: None,
parallel_phase2_config: None,
parallel_roa_worker_pool: None,
ccr_accumulator: None,
persist_vcir: true,
enable_roa_validation_cache: false,
publication_point_cache_observe_only: false,
enable_publication_point_validation_cache: true,
};
let cached = cache_runner
.run_publication_point(&handle)
.expect("publication-point cache run");
assert_eq!(cached.source, PublicationPointSource::PublicationPointCache);
assert_eq!(cached.objects.vrps, first.objects.vrps);
assert_eq!(cached.objects.aspas, first.objects.aspas);
assert_eq!(
cached.discovered_children.len(),
first.discovered_children.len()
);
assert!(!cached.cir_cached_objects.is_empty());
}
fn seed_publication_point_cache_projection(
store: &RocksStore,
policy: &Policy,
ca: &CaInstanceHandle,
validation_time: time::OffsetDateTime,
) -> ValidatedCaInstanceResult {
let child_bytes = b"child-cert".to_vec();
let child_hash = sha256_hex(&child_bytes);
let vcir = sample_vcir_for_projection(validation_time, &child_hash);
store
.put_blob_bytes_batch(&[
(child_hash, child_bytes),
(sha256_hex(b"manifest-bytes"), b"manifest-bytes".to_vec()),
])
.expect("put cache bytes");
store
.put_repository_view_entry(&RepositoryViewEntry {
rsync_uri: ca.manifest_rsync_uri.clone(),
current_hash: Some(sha256_hex(b"manifest-bytes")),
repository_source: Some(ca.publication_point_rsync_uri.clone()),
object_type: Some("mft".to_string()),
state: RepositoryViewState::Present,
})
.expect("put manifest current view");
let projection = PublicationPointCacheProjection::from_vcir_with_context(
&vcir,
ca.publication_point_rsync_uri.clone(),
ca.ca_certificate_rsync_uri.clone(),
sha256_32(&ca.ca_certificate_der),
sha256_32(b"manifest-bytes"),
ta_context_digest_for_ca(ca),
parent_context_digest_for_ca(ca),
publication_point_cache_policy_fingerprint(policy),
)
.expect("build publication point projection");
store
.put_vcir_with_publication_point_cache_projection(&vcir, Some(&projection))
.expect("put publication point projection");
vcir
}
fn publication_point_cache_fixture_ca() -> CaInstanceHandle {
CaInstanceHandle {
depth: 1,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate_der: b"ca-cert".to_vec(),
ca_certificate_rsync_uri: Some("rsync://example.test/repo/issuer/issuer.cer".to_string()),
effective_ip_resources: None,
effective_as_resources: None,
rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(),
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
rrdp_notification_uri: Some("https://example.test/notification.xml".to_string()),
}
}
#[test]
fn runner_publication_point_cache_reuses_projection_outputs_children_and_ccr() {
let store_dir = tempfile::tempdir().expect("store dir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
let policy = Policy::default();
let validation_time = time::OffsetDateTime::UNIX_EPOCH + time::Duration::minutes(1);
let ca = publication_point_cache_fixture_ca();
let vcir = seed_publication_point_cache_projection(&store, &policy, &ca, validation_time);
let timing = crate::analysis::timing::TimingHandle::new(crate::analysis::timing::TimingMeta {
recorded_at_utc_rfc3339: "2026-01-01T00:00:00Z".to_string(),
validation_time_utc_rfc3339: "2026-01-01T00:00:00Z".to_string(),
tal_url: None,
db_path: None,
});
let runner = Rpkiv1PublicationPointRunner {
store: &store,
policy: &policy,
http_fetcher: &NeverHttpFetcher,
rsync_fetcher: &FailingRsyncFetcher,
validation_time,
timing: Some(timing.clone()),
download_log: None,
replay_archive_index: None,
replay_delta_index: None,
rrdp_dedup: false,
rrdp_repo_cache: Mutex::new(HashMap::new()),
rsync_dedup: false,
rsync_repo_cache: Mutex::new(HashMap::new()),
current_repo_index: None,
repo_sync_runtime: None,
parallel_phase2_config: None,
parallel_roa_worker_pool: None,
ccr_accumulator: Some(Mutex::new(CcrAccumulator::new(Vec::new()))),
persist_vcir: true,
enable_roa_validation_cache: false,
publication_point_cache_observe_only: false,
enable_publication_point_validation_cache: true,
};
let result = runner
.observe_or_reuse_publication_point_cache(&ca, Some("rrdp"), Some("delta"), 7, None, &[])
.expect("cache result");
assert_eq!(result.source, PublicationPointSource::PublicationPointCache);
assert_eq!(result.objects.vrps.len(), 1);
assert_eq!(result.objects.aspas.len(), 1);
assert_eq!(result.objects.router_keys.len(), 1);
assert_eq!(result.discovered_children.len(), 1);
assert_eq!(
result.discovered_children[0].handle.manifest_rsync_uri,
vcir.child_entries[0].child_manifest_rsync_uri
);
assert!(result.cir_fresh_objects.is_empty());
assert!(!result.cir_cached_objects.is_empty());
assert_eq!(
runner
.ccr_accumulator_snapshot()
.expect("ccr accumulator")
.manifest_count(),
1
);
let counts = timing.counts_snapshot();
assert_eq!(
counts.get("publication_point_cache_reuse_hits").copied(),
Some(1)
);
assert_eq!(
counts
.get("publication_point_cache_outputs_reused")
.copied(),
Some(3)
);
assert_eq!(
counts
.get("publication_point_cache_children_reused")
.copied(),
Some(1)
);
}
#[test]
fn runner_publication_point_cache_blocks_parent_policy_and_output_time_mismatch() {
let store_dir = tempfile::tempdir().expect("store dir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
let policy = Policy::default();
let validation_time = time::OffsetDateTime::UNIX_EPOCH + time::Duration::minutes(1);
let ca = publication_point_cache_fixture_ca();
seed_publication_point_cache_projection(&store, &policy, &ca, validation_time);
let timing = crate::analysis::timing::TimingHandle::new(crate::analysis::timing::TimingMeta {
recorded_at_utc_rfc3339: "2026-01-01T00:00:00Z".to_string(),
validation_time_utc_rfc3339: "2026-01-01T00:00:00Z".to_string(),
tal_url: None,
db_path: None,
});
let mut parent_changed_ca = ca.clone();
parent_changed_ca.parent_manifest_rsync_uri =
Some("rsync://example.test/repo/other-parent.mft".to_string());
let parent_changed_runner = Rpkiv1PublicationPointRunner {
store: &store,
policy: &policy,
http_fetcher: &NeverHttpFetcher,
rsync_fetcher: &FailingRsyncFetcher,
validation_time,
timing: Some(timing.clone()),
download_log: None,
replay_archive_index: None,
replay_delta_index: None,
rrdp_dedup: false,
rrdp_repo_cache: Mutex::new(HashMap::new()),
rsync_dedup: false,
rsync_repo_cache: Mutex::new(HashMap::new()),
current_repo_index: None,
repo_sync_runtime: None,
parallel_phase2_config: None,
parallel_roa_worker_pool: None,
ccr_accumulator: None,
persist_vcir: true,
enable_roa_validation_cache: false,
publication_point_cache_observe_only: false,
enable_publication_point_validation_cache: true,
};
assert!(
parent_changed_runner
.observe_or_reuse_publication_point_cache(
&parent_changed_ca,
Some("rrdp"),
Some("delta"),
7,
None,
&[]
)
.is_none()
);
assert_eq!(
timing
.counts_snapshot()
.get("publication_point_cache_miss_parent_context_mismatch")
.copied(),
Some(1)
);
let strict_policy = Policy {
strict: crate::policy::StrictPolicy::all(),
..Policy::default()
};
let policy_changed_runner = Rpkiv1PublicationPointRunner {
store: &store,
policy: &strict_policy,
http_fetcher: &NeverHttpFetcher,
rsync_fetcher: &FailingRsyncFetcher,
validation_time,
timing: Some(timing.clone()),
download_log: None,
replay_archive_index: None,
replay_delta_index: None,
rrdp_dedup: false,
rrdp_repo_cache: Mutex::new(HashMap::new()),
rsync_dedup: false,
rsync_repo_cache: Mutex::new(HashMap::new()),
current_repo_index: None,
repo_sync_runtime: None,
parallel_phase2_config: None,
parallel_roa_worker_pool: None,
ccr_accumulator: None,
persist_vcir: true,
enable_roa_validation_cache: false,
publication_point_cache_observe_only: false,
enable_publication_point_validation_cache: true,
};
assert!(
policy_changed_runner
.observe_or_reuse_publication_point_cache(
&ca,
Some("rrdp"),
Some("delta"),
7,
None,
&[]
)
.is_none()
);
assert_eq!(
timing
.counts_snapshot()
.get("publication_point_cache_miss_policy_mismatch")
.copied(),
Some(1)
);
let output_expired_runner = Rpkiv1PublicationPointRunner {
store: &store,
policy: &policy,
http_fetcher: &NeverHttpFetcher,
rsync_fetcher: &FailingRsyncFetcher,
validation_time: validation_time + time::Duration::minutes(40),
timing: Some(timing.clone()),
download_log: None,
replay_archive_index: None,
replay_delta_index: None,
rrdp_dedup: false,
rrdp_repo_cache: Mutex::new(HashMap::new()),
rsync_dedup: false,
rsync_repo_cache: Mutex::new(HashMap::new()),
current_repo_index: None,
repo_sync_runtime: None,
parallel_phase2_config: None,
parallel_roa_worker_pool: None,
ccr_accumulator: None,
persist_vcir: true,
enable_roa_validation_cache: false,
publication_point_cache_observe_only: false,
enable_publication_point_validation_cache: true,
};
assert!(
output_expired_runner
.observe_or_reuse_publication_point_cache(
&ca,
Some("rrdp"),
Some("delta"),
7,
None,
&[]
)
.is_none()
);
assert_eq!(
timing
.counts_snapshot()
.get("publication_point_cache_miss_output_time_gate")
.copied(),
Some(1)
);
}
#[test]
fn runner_rsync_dedup_skips_second_sync_for_same_base() {
let fixture_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
@@ -1789,6 +2252,8 @@ fn runner_rsync_dedup_skips_second_sync_for_same_base() {
ccr_accumulator: None,
persist_vcir: true,
enable_roa_validation_cache: false,
publication_point_cache_observe_only: false,
enable_publication_point_validation_cache: false,
};
let first = runner.run_publication_point(&handle).expect("first run ok");
@@ -1903,6 +2368,8 @@ fn runner_rsync_dedup_skips_second_sync_for_same_module_scope() {
ccr_accumulator: None,
persist_vcir: true,
enable_roa_validation_cache: false,
publication_point_cache_observe_only: false,
enable_publication_point_validation_cache: false,
};
let first = runner.run_publication_point(&handle).expect("first run ok");
@@ -2020,6 +2487,8 @@ fn runner_rsync_dedup_works_in_rsync_only_mode_even_when_rrdp_notify_exists() {
ccr_accumulator: None,
persist_vcir: true,
enable_roa_validation_cache: false,
publication_point_cache_observe_only: false,
enable_publication_point_validation_cache: false,
};
let first = runner.run_publication_point(&handle).expect("first run ok");
@@ -2108,6 +2577,8 @@ fn runner_when_repo_sync_fails_uses_current_instance_vcir_and_keeps_children_emp
ccr_accumulator: None,
persist_vcir: true,
enable_roa_validation_cache: false,
publication_point_cache_observe_only: false,
enable_publication_point_validation_cache: false,
};
let first = ok_runner
.run_publication_point(&handle)
@@ -2140,6 +2611,8 @@ fn runner_when_repo_sync_fails_uses_current_instance_vcir_and_keeps_children_emp
ccr_accumulator: None,
persist_vcir: true,
enable_roa_validation_cache: false,
publication_point_cache_observe_only: false,
enable_publication_point_validation_cache: false,
};
let second = bad_runner
.run_publication_point(&handle)
@@ -3089,6 +3562,8 @@ fn runner_roa_validation_cache_uses_projection_not_full_vcir_fallback() {
ccr_accumulator: None,
persist_vcir: true,
enable_roa_validation_cache: true,
publication_point_cache_observe_only: false,
enable_publication_point_validation_cache: false,
};
assert!(
@@ -3821,6 +4296,8 @@ fn runner_dedup_paths_execute_with_timing_enabled() {
ccr_accumulator: None,
persist_vcir: true,
enable_roa_validation_cache: false,
publication_point_cache_observe_only: false,
enable_publication_point_validation_cache: false,
};
let first = runner_rrdp
.run_publication_point(&handle)
@@ -3856,6 +4333,8 @@ fn runner_dedup_paths_execute_with_timing_enabled() {
ccr_accumulator: None,
persist_vcir: true,
enable_roa_validation_cache: false,
publication_point_cache_observe_only: false,
enable_publication_point_validation_cache: false,
};
let third = runner_rsync
.run_publication_point(&handle)