20260607 完成transport预热、roa cache和finalize长尾优化

This commit is contained in:
yuyr 2026-06-07 20:35:47 +08:00
parent 4045f9a3a5
commit 8e6e2f1318
24 changed files with 2377 additions and 155 deletions

View File

@ -44,11 +44,18 @@ RPKI_PROGRESS_LOG=1
# progress log 慢步骤阈值,单位秒。
RPKI_PROGRESS_SLOW_SECS=10
# phase2 stage fresh 慢发布点阈值,单位毫秒。
RPKI_PROGRESS_STAGE_FRESH_SLOW_MS=1000
# phase2 PP 控制面慢发布点阈值,单位毫秒。
RPKI_PROGRESS_PP_CONTROL_SLOW_MS=100
# 是否在运行前尝试禁用 rpki-client timer 并杀掉竞争 RP 进程。
DISABLE_COMPETING_RPS=1
# 传给 rpki 子进程的额外参数。多个参数用空格分隔。
# 示例RPKI_EXTRA_ARGS="--enable-roa-validation-cache"
# 实验性 transport 预热RPKI_EXTRA_ARGS="--enable-transport-request-prefetch --enable-roa-validation-cache"
RPKI_EXTRA_ARGS=""
# 是否为每轮输出 timing profile 到 runs/run_xxxx/analyze/timing.json。

View File

@ -22,6 +22,8 @@ FAILURE_SNAPSHOT_RESET="${FAILURE_SNAPSHOT_RESET:-1}"
DB_STATS_EXACT_EVERY="${DB_STATS_EXACT_EVERY:-3}"
RPKI_PROGRESS_LOG="${RPKI_PROGRESS_LOG:-1}"
RPKI_PROGRESS_SLOW_SECS="${RPKI_PROGRESS_SLOW_SECS:-10}"
RPKI_PROGRESS_STAGE_FRESH_SLOW_MS="${RPKI_PROGRESS_STAGE_FRESH_SLOW_MS:-1000}"
RPKI_PROGRESS_PP_CONTROL_SLOW_MS="${RPKI_PROGRESS_PP_CONTROL_SLOW_MS:-100}"
DISABLE_COMPETING_RPS="${DISABLE_COMPETING_RPS:-1}"
RPKI_EXTRA_ARGS="${RPKI_EXTRA_ARGS:-}"
RPKI_ANALYZE="${RPKI_ANALYZE:-0}"
@ -571,6 +573,8 @@ run_one_round() {
env \
RPKI_PROGRESS_LOG="$RPKI_PROGRESS_LOG" \
RPKI_PROGRESS_SLOW_SECS="$RPKI_PROGRESS_SLOW_SECS" \
RPKI_PROGRESS_STAGE_FRESH_SLOW_MS="$RPKI_PROGRESS_STAGE_FRESH_SLOW_MS" \
RPKI_PROGRESS_PP_CONTROL_SLOW_MS="$RPKI_PROGRESS_PP_CONTROL_SLOW_MS" \
"$RPKI_DAEMON_BIN" "${daemon_args[@]}" -- "${CHILD_ARGS[@]}" \
> "$run_dir/daemon-stdout.log" 2> "$run_dir/daemon-stderr.log"
daemon_exit_code=$?

View File

@ -5,7 +5,8 @@ use std::path::{Path, PathBuf};
use rocksdb::{DB, IteratorMode, Options};
use rpki::storage::{
ALL_COLUMN_FAMILY_NAMES, CF_MANIFEST_REPLAY_META, CF_RAW_BY_HASH, CF_REPOSITORY_VIEW,
CF_RRDP_SOURCE, CF_RRDP_SOURCE_MEMBER, CF_RRDP_URI_OWNER, CF_VCIR, column_family_descriptors,
CF_ROA_CACHE_PROJECTION, CF_RRDP_SOURCE, CF_RRDP_SOURCE_MEMBER, CF_RRDP_URI_OWNER,
CF_TRANSPORT_PREFETCH, CF_VCIR, column_family_descriptors,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@ -98,7 +99,7 @@ Output:
Output groups:
- current_repository_view: repository_view + raw_by_hash
- current_validation_state: vcir + manifest_replay_meta
- current_validation_state: vcir + manifest_replay_meta + roa_cache_projection + transport_prefetch
- current_rrdp_state: rrdp_source + rrdp_source_member + rrdp_uri_owner
"
)
@ -182,7 +183,9 @@ fn collect_db_file_stats(db_path: &Path) -> Result<DbFileStats, Box<dyn std::err
fn cf_group(cf_name: &str) -> CfGroup {
match cf_name {
CF_REPOSITORY_VIEW | CF_RAW_BY_HASH => CfGroup::CurrentRepositoryView,
CF_VCIR | CF_MANIFEST_REPLAY_META => CfGroup::CurrentValidationState,
CF_VCIR | CF_MANIFEST_REPLAY_META | CF_ROA_CACHE_PROJECTION | CF_TRANSPORT_PREFETCH => {
CfGroup::CurrentValidationState
}
CF_RRDP_SOURCE | CF_RRDP_SOURCE_MEMBER | CF_RRDP_URI_OWNER => CfGroup::CurrentRrdpState,
_ => CfGroup::LegacyCompatibility,
}
@ -374,6 +377,14 @@ mod tests {
cf_group(CF_MANIFEST_REPLAY_META),
CfGroup::CurrentValidationState
);
assert_eq!(
cf_group(CF_ROA_CACHE_PROJECTION),
CfGroup::CurrentValidationState
);
assert_eq!(
cf_group(CF_TRANSPORT_PREFETCH),
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("unknown_legacy"), CfGroup::LegacyCompatibility);
@ -386,11 +397,12 @@ mod tests {
(CF_RAW_BY_HASH, 7),
(CF_VCIR, 11),
(CF_MANIFEST_REPLAY_META, 13),
(CF_ROA_CACHE_PROJECTION, 17),
(CF_RRDP_SOURCE_MEMBER, 19),
]);
assert_eq!(grouped.get(&CfGroup::CurrentRepositoryView), Some(&12));
assert_eq!(grouped.get(&CfGroup::CurrentValidationState), Some(&24));
assert_eq!(grouped.get(&CfGroup::CurrentValidationState), Some(&41));
assert_eq!(grouped.get(&CfGroup::CurrentRrdpState), Some(&19));
assert_eq!(grouped.get(&CfGroup::LegacyCompatibility), None);
}

View File

@ -50,6 +50,7 @@ use std::sync::Arc;
struct RunStageTiming {
validation_ms: u64,
enable_roa_validation_cache: bool,
enable_transport_request_prefetch: bool,
report_build_ms: u64,
report_write_ms: Option<u64>,
ccr_build_ms: Option<u64>,
@ -123,6 +124,7 @@ pub struct CliArgs {
pub skip_report_build: bool,
pub skip_vcir_persist: bool,
pub enable_roa_validation_cache: bool,
pub enable_transport_request_prefetch: bool,
pub ccr_out_path: Option<PathBuf>,
pub vrps_csv_out_path: Option<PathBuf>,
pub vaps_csv_out_path: Option<PathBuf>,
@ -179,6 +181,8 @@ Options:
--skip-vcir-persist Skip VCIR persistence/projection building for compare-only runs
--enable-roa-validation-cache
Reuse accepted ROA validation outputs from previous VCIR records (default: off)
--enable-transport-request-prefetch
Experimental: prefetch previous run transport repo requests before tree traversal
--ccr-out <path> Write CCR DER ContentInfo to this path (optional)
--vrps-csv-out <path> Write VRP compare-view CSV directly from validation output (optional; requires --vaps-csv-out)
--vaps-csv-out <path> Write VAP compare-view CSV directly from validation output (optional; requires --vrps-csv-out)
@ -259,6 +263,7 @@ pub fn parse_args(argv: &[String]) -> Result<CliArgs, String> {
let mut skip_report_build: bool = false;
let mut skip_vcir_persist: bool = false;
let mut enable_roa_validation_cache: bool = false;
let mut enable_transport_request_prefetch: bool = false;
let mut ccr_out_path: Option<PathBuf> = None;
let mut vrps_csv_out_path: Option<PathBuf> = None;
let mut vaps_csv_out_path: Option<PathBuf> = None;
@ -464,6 +469,9 @@ pub fn parse_args(argv: &[String]) -> Result<CliArgs, String> {
"--enable-roa-validation-cache" => {
enable_roa_validation_cache = true;
}
"--enable-transport-request-prefetch" => {
enable_transport_request_prefetch = true;
}
"--ccr-out" => {
i += 1;
let v = argv.get(i).ok_or("--ccr-out requires a value")?;
@ -894,6 +902,7 @@ pub fn parse_args(argv: &[String]) -> Result<CliArgs, String> {
skip_report_build,
skip_vcir_persist,
enable_roa_validation_cache,
enable_transport_request_prefetch,
ccr_out_path,
vrps_csv_out_path,
vaps_csv_out_path,
@ -1954,6 +1963,7 @@ pub fn run(argv: &[String]) -> Result<(), String> {
persist_vcir: !args.skip_vcir_persist,
build_ccr_accumulator: args.ccr_out_path.is_some(),
enable_roa_validation_cache: args.enable_roa_validation_cache,
enable_transport_request_prefetch: args.enable_transport_request_prefetch,
};
let replay_mode = args.payload_replay_archive.is_some();
let delta_replay_mode = args.payload_base_archive.is_some();
@ -2426,6 +2436,7 @@ pub fn run(argv: &[String]) -> Result<(), String> {
let stage_timing = RunStageTiming {
validation_ms,
enable_roa_validation_cache: args.enable_roa_validation_cache,
enable_transport_request_prefetch: args.enable_transport_request_prefetch,
report_build_ms,
report_write_ms,
ccr_build_ms,

View File

@ -139,6 +139,22 @@ fn parse_accepts_enable_roa_validation_cache() {
assert!(args.enable_roa_validation_cache);
}
#[test]
fn parse_accepts_enable_transport_request_prefetch() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-path".to_string(),
"x.tal".to_string(),
"--ta-path".to_string(),
"x.cer".to_string(),
"--enable-transport-request-prefetch".to_string(),
];
let args = parse_args(&argv).expect("parse args");
assert!(args.enable_transport_request_prefetch);
}
#[test]
fn parse_disables_roa_validation_cache_by_default() {
let argv = vec![
@ -152,6 +168,7 @@ fn parse_disables_roa_validation_cache_by_default() {
];
let args = parse_args(&argv).expect("parse args");
assert!(!args.enable_roa_validation_cache);
assert!(!args.enable_transport_request_prefetch);
}
#[test]
@ -1535,6 +1552,7 @@ fn run_report_task_and_stage_timing_work() {
let stage_timing = RunStageTiming {
validation_ms: 1,
enable_roa_validation_cache: false,
enable_transport_request_prefetch: false,
report_build_ms: report_output.report_build_ms,
report_write_ms: report_output.report_write_ms,
ccr_build_ms: Some(2),
@ -1631,6 +1649,7 @@ fn stage_timing_serializes_memory_telemetry() {
let stage_timing = RunStageTiming {
validation_ms: 1,
enable_roa_validation_cache: true,
enable_transport_request_prefetch: true,
report_build_ms: 2,
report_write_ms: None,
ccr_build_ms: None,

View File

@ -6,4 +6,5 @@ pub mod repo_scheduler;
pub mod repo_worker;
pub mod run_coordinator;
pub mod stats;
pub mod transport_prefetch;
pub mod types;

View File

@ -4,6 +4,9 @@ use std::time::Duration;
use crate::parallel::repo_scheduler::TransportRequestAction;
use crate::parallel::repo_worker::{RepoTransportExecutor, RepoTransportWorkerPool};
use crate::parallel::run_coordinator::GlobalRunCoordinator;
use crate::parallel::transport_prefetch::{
TransportPrefetchDispatchStats, TransportPrefetchRecorder, TransportPrefetchSnapshot,
};
use crate::parallel::types::{
RepoIdentity, RepoRequester, RepoRuntimeState, RepoTransportMode, RepoTransportResultEnvelope,
RepoTransportResultKind,
@ -70,11 +73,20 @@ pub trait RepoSyncRuntime: Send + Sync {
&self,
children: &[DiscoveredChildCaInstance],
) -> Result<(), String>;
fn prefetch_transport_requests(
&self,
snapshot: &TransportPrefetchSnapshot,
validation_time: time::OffsetDateTime,
) -> Result<TransportPrefetchDispatchStats, String>;
fn transport_prefetch_snapshot(&self) -> TransportPrefetchSnapshot;
}
pub struct Phase1RepoSyncRuntime<E: RepoTransportExecutor> {
coordinator: Mutex<GlobalRunCoordinator>,
worker_pool: Mutex<RepoTransportWorkerPool<E>>,
transport_prefetch_recorder: Option<Mutex<TransportPrefetchRecorder>>,
rsync_scope_resolver: Arc<dyn Fn(&str) -> String + Send + Sync>,
rsync_failure_scope_resolver: Arc<dyn Fn(&str) -> Option<String> + Send + Sync>,
sync_preference: SyncPreference,
@ -106,6 +118,26 @@ impl<E: RepoTransportExecutor> Phase1RepoSyncRuntime<E> {
Self {
coordinator: Mutex::new(coordinator),
worker_pool: Mutex::new(worker_pool),
transport_prefetch_recorder: None,
rsync_scope_resolver,
rsync_failure_scope_resolver,
sync_preference,
}
}
pub fn new_with_failure_scope_and_prefetch_recording(
coordinator: GlobalRunCoordinator,
worker_pool: RepoTransportWorkerPool<E>,
rsync_scope_resolver: Arc<dyn Fn(&str) -> String + Send + Sync>,
rsync_failure_scope_resolver: Arc<dyn Fn(&str) -> Option<String> + Send + Sync>,
sync_preference: SyncPreference,
record_transport_prefetch_requests: bool,
) -> Self {
Self {
coordinator: Mutex::new(coordinator),
worker_pool: Mutex::new(worker_pool),
transport_prefetch_recorder: record_transport_prefetch_requests
.then(|| Mutex::new(TransportPrefetchRecorder::default())),
rsync_scope_resolver,
rsync_failure_scope_resolver,
sync_preference,
@ -136,6 +168,19 @@ impl<E: RepoTransportExecutor> Phase1RepoSyncRuntime<E> {
let requester = Self::build_requester(ca);
let rsync_scope_uri = (self.rsync_scope_resolver)(&identity.rsync_base_uri);
let rsync_failure_scope_uri = (self.rsync_failure_scope_resolver)(&identity.rsync_base_uri);
if let Some(recorder) = self.transport_prefetch_recorder.as_ref() {
let mut recorder = recorder
.lock()
.expect("transport prefetch recorder lock poisoned");
recorder.record_registered_request(
&identity,
&requester,
priority,
rsync_scope_uri.clone(),
rsync_failure_scope_uri.clone(),
self.sync_preference,
);
}
let action = {
let mut coordinator = self.coordinator.lock().expect("coordinator lock poisoned");
coordinator.register_transport_request(
@ -432,6 +477,83 @@ impl<E: RepoTransportExecutor> RepoSyncRuntime for Phase1RepoSyncRuntime<E> {
}
Ok(())
}
fn prefetch_transport_requests(
&self,
snapshot: &TransportPrefetchSnapshot,
validation_time: time::OffsetDateTime,
) -> Result<TransportPrefetchDispatchStats, String> {
let mut stats = TransportPrefetchDispatchStats {
loaded_requests: snapshot.requests.len() as u64,
..TransportPrefetchDispatchStats::default()
};
for request in &snapshot.requests {
let identity = request.to_identity();
let current_rsync_scope_uri = (self.rsync_scope_resolver)(&identity.rsync_base_uri);
let current_rsync_failure_scope_uri =
(self.rsync_failure_scope_resolver)(&identity.rsync_base_uri);
if current_rsync_scope_uri != request.rsync_scope_uri
|| current_rsync_failure_scope_uri != request.rsync_failure_scope_uri
{
stats.skipped_incompatible += 1;
continue;
}
let action = {
let mut coordinator = self.coordinator.lock().expect("coordinator lock poisoned");
coordinator.register_transport_request(
identity,
request.to_requester(),
validation_time,
request.priority,
current_rsync_scope_uri,
current_rsync_failure_scope_uri,
self.sync_preference,
)
};
match action {
TransportRequestAction::Enqueue(task) => {
stats.enqueued_tasks += 1;
crate::progress_log::emit(
"phase1_repo_prefetch_enqueued",
serde_json::json!({
"repo_key_rsync_base_uri": task.repo_identity.rsync_base_uri,
"rsync_failure_scope_uri": task.rsync_failure_scope_uri,
"repo_key_notification_uri": task.repo_identity.notification_uri,
"priority": task.priority,
"transport_mode": match task.mode {
RepoTransportMode::Rrdp => "rrdp",
RepoTransportMode::Rsync => "rsync",
},
}),
);
}
TransportRequestAction::Waiting { .. } => {
stats.waiting_requests += 1;
}
TransportRequestAction::ReusedSuccess(_)
| TransportRequestAction::ReusedTerminalFailure(_) => {
stats.reused_results += 1;
}
}
}
self.drain_pending_transport_tasks()?;
Ok(stats)
}
fn transport_prefetch_snapshot(&self) -> TransportPrefetchSnapshot {
self.transport_prefetch_recorder
.as_ref()
.map(|recorder| {
recorder
.lock()
.expect("transport prefetch recorder lock poisoned")
.snapshot(self.sync_preference)
})
.unwrap_or_else(|| TransportPrefetchSnapshot::new(self.sync_preference, Vec::new()))
}
}
fn outcome_from_transport_result(
@ -501,6 +623,7 @@ mod tests {
RepoTransportExecutor, RepoTransportWorkerPool, RepoWorkerPoolConfig,
};
use crate::parallel::run_coordinator::GlobalRunCoordinator;
use crate::parallel::transport_prefetch::TransportPrefetchSnapshot;
use crate::parallel::types::{
RepoRuntimeState, RepoTransportMode, RepoTransportResultEnvelope, RepoTransportResultKind,
RepoTransportTask, TalInputSpec,
@ -889,6 +1012,218 @@ mod tests {
assert_eq!(count.load(Ordering::SeqCst), 2);
}
#[test]
fn phase1_runtime_records_prefetch_snapshot_only_when_enabled() {
let coordinator = GlobalRunCoordinator::new(
ParallelPhase1Config::default(),
vec![TalInputSpec::from_url("https://example.test/arin.tal")],
);
let pool = RepoTransportWorkerPool::new(
RepoWorkerPoolConfig { max_workers: 1 },
SuccessTransportExecutor,
)
.expect("pool");
let runtime = Phase1RepoSyncRuntime::new_with_failure_scope_and_prefetch_recording(
coordinator,
pool,
Arc::new(|base: &str| base.to_string()),
Arc::new(|_base: &str| Some("rsync://example.test/".to_string())),
SyncPreference::RrdpThenRsync,
true,
);
let ca = sample_ca("rsync://example.test/repo/root.mft");
let _ = runtime
.request_publication_point_repo(&ca, 0)
.expect("request repo");
let snapshot = runtime.transport_prefetch_snapshot();
assert_eq!(snapshot.requests.len(), 1);
assert_eq!(
snapshot.requests[0].repo_identity.rsync_base_uri,
"rsync://example.test/repo/"
);
assert_eq!(
snapshot.requests[0].rsync_failure_scope_uri.as_deref(),
Some("rsync://example.test/")
);
let coordinator = GlobalRunCoordinator::new(
ParallelPhase1Config::default(),
vec![TalInputSpec::from_url("https://example.test/arin.tal")],
);
let pool = RepoTransportWorkerPool::new(
RepoWorkerPoolConfig { max_workers: 1 },
SuccessTransportExecutor,
)
.expect("pool");
let runtime = Phase1RepoSyncRuntime::new(
coordinator,
pool,
Arc::new(|base: &str| base.to_string()),
SyncPreference::RrdpThenRsync,
);
let _ = runtime
.request_publication_point_repo(&ca, 0)
.expect("request repo without recording");
assert!(runtime.transport_prefetch_snapshot().requests.is_empty());
}
#[test]
fn phase1_runtime_prefetch_dispatches_and_later_request_waits_on_same_task() {
let count = Arc::new(AtomicUsize::new(0));
let coordinator = GlobalRunCoordinator::new(
ParallelPhase1Config::default(),
vec![TalInputSpec::from_url("https://example.test/arin.tal")],
);
let pool = RepoTransportWorkerPool::new(
RepoWorkerPoolConfig { max_workers: 1 },
CountingSuccessTransportExecutor {
count: Arc::clone(&count),
},
)
.expect("pool");
let runtime = Phase1RepoSyncRuntime::new_with_failure_scope_and_prefetch_recording(
coordinator,
pool,
Arc::new(|base: &str| base.to_string()),
Arc::new(|_base: &str| Some("rsync://example.test/".to_string())),
SyncPreference::RrdpThenRsync,
true,
);
let ca = sample_ca("rsync://example.test/repo/root.mft");
let mut recorder =
crate::parallel::transport_prefetch::TransportPrefetchRecorder::default();
recorder.record_registered_request(
&super::Phase1RepoSyncRuntime::<CountingSuccessTransportExecutor>::build_identity(&ca),
&super::Phase1RepoSyncRuntime::<CountingSuccessTransportExecutor>::build_requester(&ca),
0,
"rsync://example.test/repo/".to_string(),
Some("rsync://example.test/".to_string()),
SyncPreference::RrdpThenRsync,
);
let snapshot = recorder.snapshot(SyncPreference::RrdpThenRsync);
let stats = runtime
.prefetch_transport_requests(&snapshot, time::OffsetDateTime::UNIX_EPOCH)
.expect("prefetch transport requests");
assert_eq!(stats.loaded_requests, 1);
assert_eq!(stats.enqueued_tasks, 1);
let status = runtime
.request_publication_point_repo(&ca, 0)
.expect("request after prefetch");
assert!(matches!(
status,
super::RepoSyncRequestStatus::Pending {
state: RepoRuntimeState::WaitingRrdp,
..
}
));
let _ = runtime
.recv_repo_result_timeout(Duration::from_secs(1))
.expect("repo event")
.expect("event");
assert_eq!(count.load(Ordering::SeqCst), 1);
}
#[test]
fn phase1_runtime_does_not_persist_prefetch_only_requests() {
let coordinator = GlobalRunCoordinator::new(
ParallelPhase1Config::default(),
vec![TalInputSpec::from_url("https://example.test/arin.tal")],
);
let pool = RepoTransportWorkerPool::new(
RepoWorkerPoolConfig { max_workers: 1 },
SuccessTransportExecutor,
)
.expect("pool");
let runtime = Phase1RepoSyncRuntime::new_with_failure_scope_and_prefetch_recording(
coordinator,
pool,
Arc::new(|base: &str| base.to_string()),
Arc::new(|_base: &str| Some("rsync://example.test/".to_string())),
SyncPreference::RrdpThenRsync,
true,
);
let snapshot = TransportPrefetchSnapshot::new(
SyncPreference::RrdpThenRsync,
vec![crate::parallel::transport_prefetch::TransportPrefetchRequest::from_registered_request(
&crate::parallel::types::RepoIdentity::new(
Some("https://example.test/notify.xml".to_string()),
"rsync://example.test/repo/",
),
&crate::parallel::types::RepoRequester::with_tal_rir(
"arin",
"arin",
"rsync://example.test/repo/root.mft",
"rsync://example.test/repo/",
"arin:root",
),
0,
"rsync://example.test/repo/".to_string(),
Some("rsync://example.test/".to_string()),
SyncPreference::RrdpThenRsync,
)],
);
let stats = runtime
.prefetch_transport_requests(&snapshot, time::OffsetDateTime::UNIX_EPOCH)
.expect("prefetch transport requests");
assert_eq!(stats.enqueued_tasks, 1);
assert!(
runtime.transport_prefetch_snapshot().requests.is_empty(),
"prefetch-only requests should not be carried forward forever"
);
}
#[test]
fn phase1_runtime_prefetch_skips_requests_when_scope_resolver_differs() {
let coordinator = GlobalRunCoordinator::new(
ParallelPhase1Config::default(),
vec![TalInputSpec::from_url("https://example.test/arin.tal")],
);
let pool = RepoTransportWorkerPool::new(
RepoWorkerPoolConfig { max_workers: 1 },
SuccessTransportExecutor,
)
.expect("pool");
let runtime = Phase1RepoSyncRuntime::new_with_failure_scope_and_prefetch_recording(
coordinator,
pool,
Arc::new(|_base: &str| "rsync://example.test/different/".to_string()),
Arc::new(|_base: &str| Some("rsync://example.test/".to_string())),
SyncPreference::RrdpThenRsync,
true,
);
let snapshot = TransportPrefetchSnapshot::new(
SyncPreference::RrdpThenRsync,
vec![crate::parallel::transport_prefetch::TransportPrefetchRequest::from_registered_request(
&crate::parallel::types::RepoIdentity::new(
Some("https://example.test/notify.xml".to_string()),
"rsync://example.test/repo/",
),
&crate::parallel::types::RepoRequester::with_tal_rir(
"arin",
"arin",
"rsync://example.test/repo/root.mft",
"rsync://example.test/repo/",
"arin:root",
),
0,
"rsync://example.test/repo/".to_string(),
Some("rsync://example.test/".to_string()),
SyncPreference::RrdpThenRsync,
)],
);
let stats = runtime
.prefetch_transport_requests(&snapshot, time::OffsetDateTime::UNIX_EPOCH)
.expect("prefetch transport requests");
assert_eq!(stats.loaded_requests, 1);
assert_eq!(stats.enqueued_tasks, 0);
assert_eq!(stats.skipped_incompatible, 1);
}
#[test]
fn phase1_runtime_transitions_rrdp_failure_to_rsync_success() {
let rrdp_count = Arc::new(AtomicUsize::new(0));

View File

@ -0,0 +1,400 @@
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use crate::parallel::types::{
RepoDedupKey, RepoIdentity, RepoRequester, RepoTransportMode, RepoTransportTask,
};
use crate::policy::SyncPreference;
pub const TRANSPORT_PREFETCH_SCHEMA_VERSION: u32 = 1;
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct TransportPrefetchSnapshot {
pub schema_version: u32,
pub generated_at_unix: i64,
pub sync_preference: SyncPreference,
pub requests: Vec<TransportPrefetchRequest>,
}
impl TransportPrefetchSnapshot {
pub fn new(sync_preference: SyncPreference, requests: Vec<TransportPrefetchRequest>) -> Self {
Self {
schema_version: TRANSPORT_PREFETCH_SCHEMA_VERSION,
generated_at_unix: time::OffsetDateTime::now_utc().unix_timestamp(),
sync_preference,
requests,
}
}
pub fn is_compatible_with(&self, sync_preference: SyncPreference) -> bool {
self.schema_version == TRANSPORT_PREFETCH_SCHEMA_VERSION
&& self.sync_preference == sync_preference
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TransportPrefetchRequest {
pub dedup_key: TransportPrefetchDedupKey,
pub rsync_scope_uri: String,
pub rsync_failure_scope_uri: Option<String>,
pub repo_identity: TransportPrefetchRepoIdentity,
pub mode: TransportPrefetchMode,
pub tal_id: String,
pub rir_id: String,
pub priority: u8,
pub requesters: Vec<TransportPrefetchRequester>,
}
impl TransportPrefetchRequest {
pub fn from_registered_request(
identity: &RepoIdentity,
requester: &RepoRequester,
priority: u8,
rsync_scope_uri: String,
rsync_failure_scope_uri: Option<String>,
sync_preference: SyncPreference,
) -> Self {
let (dedup_key, mode) = if sync_preference == SyncPreference::RrdpThenRsync {
if let Some(notification_uri) = identity.notification_uri.clone() {
(
TransportPrefetchDedupKey::RrdpNotify { notification_uri },
TransportPrefetchMode::Rrdp,
)
} else {
(
TransportPrefetchDedupKey::RsyncScope {
rsync_scope_uri: rsync_scope_uri.clone(),
},
TransportPrefetchMode::Rsync,
)
}
} else {
(
TransportPrefetchDedupKey::RsyncScope {
rsync_scope_uri: rsync_scope_uri.clone(),
},
TransportPrefetchMode::Rsync,
)
};
Self {
dedup_key,
rsync_scope_uri,
rsync_failure_scope_uri,
repo_identity: TransportPrefetchRepoIdentity::from_identity(identity),
mode,
tal_id: requester.tal_id.clone(),
rir_id: requester.rir_id.clone(),
priority,
requesters: vec![TransportPrefetchRequester::from_requester(requester)],
}
}
pub fn from_task(task: &RepoTransportTask, rsync_scope_uri: String) -> Self {
Self {
dedup_key: TransportPrefetchDedupKey::from_repo_key(&task.dedup_key),
rsync_scope_uri,
rsync_failure_scope_uri: task.rsync_failure_scope_uri.clone(),
repo_identity: TransportPrefetchRepoIdentity::from_identity(&task.repo_identity),
mode: TransportPrefetchMode::from_mode(task.mode),
tal_id: task.tal_id.clone(),
rir_id: task.rir_id.clone(),
priority: task.priority,
requesters: task
.requesters
.iter()
.map(TransportPrefetchRequester::from_requester)
.collect(),
}
}
pub fn to_identity(&self) -> RepoIdentity {
self.repo_identity.to_identity()
}
pub fn to_requester(&self) -> RepoRequester {
self.requesters
.first()
.map(TransportPrefetchRequester::to_requester)
.unwrap_or_else(|| RepoRequester {
tal_id: self.tal_id.clone(),
rir_id: self.rir_id.clone(),
parent_node_id: None,
ca_instance_handle_id: format!(
"{}:{}",
self.tal_id, self.repo_identity.rsync_base_uri
),
publication_point_rsync_uri: self.repo_identity.rsync_base_uri.clone(),
manifest_rsync_uri: format!("{}prefetch.mft", self.repo_identity.rsync_base_uri),
})
}
fn recorder_key(&self) -> String {
self.dedup_key.stable_key()
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TransportPrefetchDedupKey {
RrdpNotify { notification_uri: String },
RsyncScope { rsync_scope_uri: String },
}
impl TransportPrefetchDedupKey {
fn from_repo_key(key: &RepoDedupKey) -> Self {
match key {
RepoDedupKey::RrdpNotify { notification_uri } => Self::RrdpNotify {
notification_uri: notification_uri.clone(),
},
RepoDedupKey::RsyncScope { rsync_scope_uri } => Self::RsyncScope {
rsync_scope_uri: rsync_scope_uri.clone(),
},
}
}
fn stable_key(&self) -> String {
match self {
Self::RrdpNotify { notification_uri } => format!("rrdp:{notification_uri}"),
Self::RsyncScope { rsync_scope_uri } => format!("rsync:{rsync_scope_uri}"),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TransportPrefetchRepoIdentity {
pub notification_uri: Option<String>,
pub rsync_base_uri: String,
}
impl TransportPrefetchRepoIdentity {
fn from_identity(identity: &RepoIdentity) -> Self {
Self {
notification_uri: identity.notification_uri.clone(),
rsync_base_uri: identity.rsync_base_uri.clone(),
}
}
fn to_identity(&self) -> RepoIdentity {
RepoIdentity::new(self.notification_uri.clone(), self.rsync_base_uri.clone())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TransportPrefetchMode {
Rrdp,
Rsync,
}
impl TransportPrefetchMode {
fn from_mode(mode: RepoTransportMode) -> Self {
match mode {
RepoTransportMode::Rrdp => Self::Rrdp,
RepoTransportMode::Rsync => Self::Rsync,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TransportPrefetchRequester {
pub tal_id: String,
pub rir_id: String,
pub parent_node_id: Option<u64>,
pub ca_instance_handle_id: String,
pub publication_point_rsync_uri: String,
pub manifest_rsync_uri: String,
}
impl TransportPrefetchRequester {
fn from_requester(requester: &RepoRequester) -> Self {
Self {
tal_id: requester.tal_id.clone(),
rir_id: requester.rir_id.clone(),
parent_node_id: requester.parent_node_id,
ca_instance_handle_id: requester.ca_instance_handle_id.clone(),
publication_point_rsync_uri: requester.publication_point_rsync_uri.clone(),
manifest_rsync_uri: requester.manifest_rsync_uri.clone(),
}
}
fn to_requester(&self) -> RepoRequester {
RepoRequester {
tal_id: self.tal_id.clone(),
rir_id: self.rir_id.clone(),
parent_node_id: self.parent_node_id,
ca_instance_handle_id: self.ca_instance_handle_id.clone(),
publication_point_rsync_uri: self.publication_point_rsync_uri.clone(),
manifest_rsync_uri: self.manifest_rsync_uri.clone(),
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct TransportPrefetchRecorder {
requests_by_key: BTreeMap<String, TransportPrefetchRequest>,
request_order: Vec<String>,
}
impl TransportPrefetchRecorder {
pub fn record_registered_request(
&mut self,
identity: &RepoIdentity,
requester: &RepoRequester,
priority: u8,
rsync_scope_uri: String,
rsync_failure_scope_uri: Option<String>,
sync_preference: SyncPreference,
) {
let request = TransportPrefetchRequest::from_registered_request(
identity,
requester,
priority,
rsync_scope_uri,
rsync_failure_scope_uri,
sync_preference,
);
self.record_request(request);
}
pub fn record_task(&mut self, task: &RepoTransportTask, rsync_scope_uri: String) {
let request = TransportPrefetchRequest::from_task(task, rsync_scope_uri);
self.record_request(request);
}
pub fn snapshot(&self, sync_preference: SyncPreference) -> TransportPrefetchSnapshot {
TransportPrefetchSnapshot::new(
sync_preference,
self.request_order
.iter()
.filter_map(|key| self.requests_by_key.get(key))
.cloned()
.collect(),
)
}
pub fn len(&self) -> usize {
self.requests_by_key.len()
}
fn record_request(&mut self, request: TransportPrefetchRequest) {
let key = request.recorder_key();
if !self.requests_by_key.contains_key(&key) {
self.request_order.push(key.clone());
self.requests_by_key.insert(key, request);
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct TransportPrefetchDispatchStats {
pub loaded_requests: u64,
pub enqueued_tasks: u64,
pub waiting_requests: u64,
pub reused_results: u64,
pub skipped_incompatible: u64,
}
#[cfg(test)]
mod tests {
use super::*;
fn requester(uri: &str) -> RepoRequester {
RepoRequester {
tal_id: "apnic".to_string(),
rir_id: "apnic".to_string(),
parent_node_id: None,
ca_instance_handle_id: format!("apnic:{uri}"),
publication_point_rsync_uri: "rsync://example.test/repo/".to_string(),
manifest_rsync_uri: uri.to_string(),
}
}
fn task(notification_uri: &str, manifest_uri: &str) -> RepoTransportTask {
RepoTransportTask {
dedup_key: RepoDedupKey::RrdpNotify {
notification_uri: notification_uri.to_string(),
},
rsync_failure_scope_uri: Some("rsync://example.test/".to_string()),
repo_identity: RepoIdentity::new(
Some(notification_uri.to_string()),
"rsync://example.test/repo/",
),
mode: RepoTransportMode::Rrdp,
tal_id: "apnic".to_string(),
rir_id: "apnic".to_string(),
validation_time: time::OffsetDateTime::UNIX_EPOCH,
priority: 0,
requesters: vec![requester(manifest_uri)],
}
}
#[test]
fn recorder_deduplicates_by_transport_key() {
let mut recorder = TransportPrefetchRecorder::default();
recorder.record_task(
&task(
"https://example.test/notification.xml",
"rsync://example.test/repo/a.mft",
),
"rsync://example.test/repo/".to_string(),
);
recorder.record_task(
&task(
"https://example.test/notification.xml",
"rsync://example.test/repo/b.mft",
),
"rsync://example.test/repo/".to_string(),
);
let snapshot = recorder.snapshot(SyncPreference::RrdpThenRsync);
assert_eq!(snapshot.requests.len(), 1);
assert_eq!(
snapshot.requests[0].requesters[0].manifest_rsync_uri,
"rsync://example.test/repo/a.mft"
);
}
#[test]
fn recorder_preserves_first_discovery_order() {
let mut recorder = TransportPrefetchRecorder::default();
recorder.record_task(
&task(
"https://z.example.test/notification.xml",
"rsync://z.example.test/repo/root.mft",
),
"rsync://z.example.test/repo/".to_string(),
);
recorder.record_task(
&task(
"https://a.example.test/notification.xml",
"rsync://a.example.test/repo/root.mft",
),
"rsync://a.example.test/repo/".to_string(),
);
let snapshot = recorder.snapshot(SyncPreference::RrdpThenRsync);
assert_eq!(
snapshot.requests[0]
.repo_identity
.notification_uri
.as_deref(),
Some("https://z.example.test/notification.xml")
);
assert_eq!(
snapshot.requests[1]
.repo_identity
.notification_uri
.as_deref(),
Some("https://a.example.test/notification.xml")
);
}
#[test]
fn snapshot_checks_schema_and_sync_preference() {
let snapshot = TransportPrefetchSnapshot::new(SyncPreference::RrdpThenRsync, Vec::new());
assert!(snapshot.is_compatible_with(SyncPreference::RrdpThenRsync));
assert!(!snapshot.is_compatible_with(SyncPreference::RsyncOnly));
}
}

View File

@ -23,6 +23,13 @@ pub fn stage_fresh_slow_threshold_ms() -> u64 {
.unwrap_or(1_000)
}
pub fn pp_control_slow_threshold_ms() -> u64 {
std::env::var("RPKI_PROGRESS_PP_CONTROL_SLOW_MS")
.ok()
.and_then(|v| v.parse::<u64>().ok())
.unwrap_or(100)
}
pub fn emit(kind: &str, payload: Value) {
if !progress_enabled() {
return;

View File

@ -2,7 +2,7 @@ mod config;
mod keys;
mod pack;
use std::collections::HashSet;
use std::collections::{HashMap, HashSet};
use std::path::Path;
use base64::Engine;
@ -15,7 +15,8 @@ use crate::data_model::rc::{AsResourceSet, IpResourceSet};
use config::*;
pub use config::{
ALL_COLUMN_FAMILY_NAMES, CF_MANIFEST_REPLAY_META, CF_RAW_BY_HASH, CF_REPOSITORY_VIEW,
CF_RRDP_SOURCE, CF_RRDP_SOURCE_MEMBER, CF_RRDP_URI_OWNER, CF_VCIR, column_family_descriptors,
CF_ROA_CACHE_PROJECTION, CF_RRDP_SOURCE, CF_RRDP_SOURCE_MEMBER, CF_RRDP_URI_OWNER,
CF_TRANSPORT_PREFETCH, CF_VCIR, column_family_descriptors,
};
use keys::*;
use pack::compute_sha256_32;
@ -865,6 +866,224 @@ fn validate_local_output_type_matches_payload(output: &VcirLocalOutput) -> Stora
Ok(())
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RoaCacheCrlProjection {
#[serde(rename = "u")]
pub uri: String,
#[serde(rename = "h")]
pub sha256: String,
}
impl RoaCacheCrlProjection {
pub fn validate_internal(&self) -> StorageResult<()> {
validate_non_empty("roa_cache_projection.crls[].uri", &self.uri)?;
validate_sha256_hex("roa_cache_projection.crls[].sha256", &self.sha256)?;
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RoaCacheLocalOutputProjection {
#[serde(rename = "e")]
pub item_effective_until: PackTime,
#[serde(rename = "c")]
#[serde(with = "serde_bytes_32")]
pub source_ee_cert_hash: [u8; 32],
#[serde(rename = "p")]
pub payload: VcirLocalOutputPayload,
#[serde(rename = "r")]
#[serde(with = "serde_bytes_32")]
pub rule_hash: [u8; 32],
}
impl RoaCacheLocalOutputProjection {
fn from_local_output(output: &VcirLocalOutput) -> Option<Self> {
if output.output_type != VcirOutputType::Vrp
|| output.source_object_type != VcirSourceObjectType::Roa
{
return None;
}
Some(Self {
item_effective_until: output.item_effective_until.clone(),
source_ee_cert_hash: output.source_ee_cert_hash,
payload: output.payload.clone(),
rule_hash: output.rule_hash,
})
}
pub fn validate_internal(&self) -> StorageResult<()> {
parse_time(
"roa_cache_projection.entries[].outputs[].item_effective_until",
&self.item_effective_until,
)?;
if !matches!(self.payload, VcirLocalOutputPayload::Vrp { .. }) {
return Err(StorageError::InvalidData {
entity: "roa_cache_projection.entries[].outputs[]",
detail: "payload must be VRP".to_string(),
});
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RoaCacheObjectProjection {
#[serde(rename = "u")]
pub source_object_uri: String,
#[serde(rename = "h")]
#[serde(with = "serde_bytes_32")]
pub source_object_hash: [u8; 32],
#[serde(rename = "o")]
pub outputs: Vec<RoaCacheLocalOutputProjection>,
}
impl RoaCacheObjectProjection {
pub fn validate_internal(&self) -> StorageResult<()> {
validate_non_empty(
"roa_cache_projection.entries[].source_object_uri",
&self.source_object_uri,
)?;
if self.outputs.is_empty() {
return Err(StorageError::InvalidData {
entity: "roa_cache_projection.entries[]",
detail: "outputs must not be empty".to_string(),
});
}
for output in &self.outputs {
output.validate_internal()?;
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RoaCacheProjection {
#[serde(rename = "m")]
pub manifest_rsync_uri: String,
#[serde(rename = "e")]
pub instance_effective_until: PackTime,
#[serde(rename = "i")]
pub issuer_ca_sha256_hex: Option<String>,
#[serde(rename = "c")]
pub crl_sha256_by_uri: Vec<RoaCacheCrlProjection>,
#[serde(rename = "r")]
pub entries: Vec<RoaCacheObjectProjection>,
}
impl RoaCacheProjection {
pub fn from_vcir(vcir: &ValidatedCaInstanceResult) -> StorageResult<Option<Self>> {
let mut issuer_ca_sha256_hex = None;
let mut crl_sha256_by_uri = Vec::new();
for artifact in &vcir.related_artifacts {
if artifact.validation_status != VcirArtifactValidationStatus::Accepted {
continue;
}
match (artifact.artifact_role, artifact.artifact_kind) {
(
VcirArtifactRole::IssuerCert | VcirArtifactRole::TrustAnchorCert,
VcirArtifactKind::Cer,
) => {
issuer_ca_sha256_hex = Some(artifact.sha256.clone());
}
(_, VcirArtifactKind::Crl) => {
if let Some(uri) = artifact.uri.as_ref() {
crl_sha256_by_uri.push(RoaCacheCrlProjection {
uri: uri.clone(),
sha256: artifact.sha256.clone(),
});
}
}
_ => {}
}
}
crl_sha256_by_uri.sort_by(|left, right| left.uri.cmp(&right.uri));
let mut entries: Vec<RoaCacheObjectProjection> = Vec::new();
let mut entry_index_by_uri: HashMap<String, usize> = HashMap::new();
for output in &vcir.local_outputs {
let Some(projected_output) = RoaCacheLocalOutputProjection::from_local_output(output)
else {
continue;
};
if let Some(entry_index) = entry_index_by_uri.get(output.source_object_uri.as_str()) {
let entry = &mut entries[*entry_index];
if entry.source_object_hash != output.source_object_hash {
return Err(StorageError::InvalidData {
entity: "roa_cache_projection.entries[]",
detail: format!(
"source object hash mismatch for {}",
output.source_object_uri
),
});
}
entry.outputs.push(projected_output);
} else {
entry_index_by_uri.insert(output.source_object_uri.clone(), entries.len());
entries.push(RoaCacheObjectProjection {
source_object_uri: output.source_object_uri.clone(),
source_object_hash: output.source_object_hash,
outputs: vec![projected_output],
});
}
}
if entries.is_empty() {
return Ok(None);
}
entries.sort_by(|left, right| left.source_object_uri.cmp(&right.source_object_uri));
let projection = Self {
manifest_rsync_uri: vcir.manifest_rsync_uri.clone(),
instance_effective_until: vcir.instance_gate.instance_effective_until.clone(),
issuer_ca_sha256_hex,
crl_sha256_by_uri,
entries,
};
projection.validate_internal()?;
Ok(Some(projection))
}
pub fn validate_internal(&self) -> StorageResult<()> {
validate_non_empty(
"roa_cache_projection.manifest_rsync_uri",
&self.manifest_rsync_uri,
)?;
parse_time(
"roa_cache_projection.instance_effective_until",
&self.instance_effective_until,
)?;
if let Some(hash) = &self.issuer_ca_sha256_hex {
validate_sha256_hex("roa_cache_projection.issuer_ca_sha256_hex", hash)?;
}
let mut seen_crls = HashSet::with_capacity(self.crl_sha256_by_uri.len());
for crl in &self.crl_sha256_by_uri {
crl.validate_internal()?;
if !seen_crls.insert(crl.uri.as_str()) {
return Err(StorageError::InvalidData {
entity: "roa_cache_projection.crls[]",
detail: format!("duplicate CRL URI: {}", crl.uri),
});
}
}
if self.entries.is_empty() {
return Err(StorageError::InvalidData {
entity: "roa_cache_projection.entries",
detail: "must not be empty".to_string(),
});
}
let mut seen_entries = HashSet::with_capacity(self.entries.len());
for entry in &self.entries {
entry.validate_internal()?;
if !seen_entries.insert(entry.source_object_uri.as_str()) {
return Err(StorageError::InvalidData {
entity: "roa_cache_projection.entries[]",
detail: format!("duplicate ROA URI: {}", entry.source_object_uri),
});
}
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum VcirArtifactRole {
@ -1126,6 +1345,8 @@ pub struct VcirReplaceTimingBreakdown {
pub vcir_value_bytes: u64,
pub replay_meta_encode_ms: u64,
pub replay_meta_value_bytes: u64,
pub roa_cache_projection_encode_ms: u64,
pub roa_cache_projection_value_bytes: u64,
pub batch_build_ms: u64,
pub write_batch_ms: u64,
pub total_encoded_bytes: u64,
@ -1134,6 +1355,7 @@ pub struct VcirReplaceTimingBreakdown {
pub rss_after_validate_kb: Option<u64>,
pub rss_after_vcir_encode_kb: Option<u64>,
pub rss_after_replay_meta_encode_kb: Option<u64>,
pub rss_after_roa_cache_projection_encode_kb: Option<u64>,
pub rss_after_write_batch_kb: Option<u64>,
}
@ -1583,6 +1805,29 @@ impl RrdpUriOwnerRecord {
}
}
fn write_roa_cache_projection_to_batch(
projection_cf: &ColumnFamily,
batch: &mut WriteBatch,
vcir: &ValidatedCaInstanceResult,
timing: Option<&mut VcirReplaceTimingBreakdown>,
) -> StorageResult<()> {
let projection_key = roa_cache_projection_key(&vcir.manifest_rsync_uri);
let projection = RoaCacheProjection::from_vcir(vcir)?;
match projection {
Some(projection) => {
let projection_value = encode_cbor(&projection, "roa_cache_projection")?;
if let Some(timing) = timing {
timing.roa_cache_projection_value_bytes = projection_value.len() as u64;
}
batch.put_cf(projection_cf, projection_key.as_bytes(), projection_value);
}
None => {
batch.delete_cf(projection_cf, projection_key.as_bytes());
}
}
Ok(())
}
impl RocksStore {
pub fn open(path: &Path) -> StorageResult<Self> {
let mut base_opts = Options::default();
@ -1979,6 +2224,7 @@ impl RocksStore {
vcir.validate_internal()?;
let vcir_cf = self.cf(CF_VCIR)?;
let replay_cf = self.cf(CF_MANIFEST_REPLAY_META)?;
let projection_cf = self.cf(CF_ROA_CACHE_PROJECTION)?;
let replay_meta = ManifestReplayMeta::from_vcir(vcir);
replay_meta.validate_internal()?;
let mut batch = WriteBatch::default();
@ -1988,6 +2234,7 @@ impl RocksStore {
let replay_key = manifest_replay_meta_key(&replay_meta.manifest_rsync_uri);
let replay_value = encode_cbor(&replay_meta, "manifest_replay_meta")?;
batch.put_cf(replay_cf, replay_key.as_bytes(), replay_value);
write_roa_cache_projection_to_batch(projection_cf, &mut batch, vcir, None)?;
self.write_batch(batch)
}
@ -2009,6 +2256,7 @@ impl RocksStore {
let batch_build_started = std::time::Instant::now();
let vcir_cf = self.cf(CF_VCIR)?;
let replay_cf = self.cf(CF_MANIFEST_REPLAY_META)?;
let projection_cf = self.cf(CF_ROA_CACHE_PROJECTION)?;
let mut batch = WriteBatch::default();
let vcir_key = vcir_key(&vcir.manifest_rsync_uri);
@ -2029,7 +2277,15 @@ impl RocksStore {
batch.put_cf(replay_cf, replay_key.as_bytes(), replay_value);
timing.rss_after_replay_meta_encode_kb = process_vm_rss_kb();
timing.total_encoded_bytes = timing.vcir_value_bytes + timing.replay_meta_value_bytes;
let projection_encode_started = std::time::Instant::now();
write_roa_cache_projection_to_batch(projection_cf, &mut batch, vcir, Some(&mut timing))?;
timing.roa_cache_projection_encode_ms =
projection_encode_started.elapsed().as_millis() as u64;
timing.rss_after_roa_cache_projection_encode_kb = process_vm_rss_kb();
timing.total_encoded_bytes = timing.vcir_value_bytes
+ timing.replay_meta_value_bytes
+ timing.roa_cache_projection_value_bytes;
timing.batch_build_ms = batch_build_started.elapsed().as_millis() as u64;
let write_batch_started = std::time::Instant::now();
@ -2075,6 +2331,53 @@ impl RocksStore {
Ok(Some(meta))
}
pub fn get_roa_cache_projection(
&self,
manifest_rsync_uri: &str,
) -> StorageResult<Option<RoaCacheProjection>> {
let cf = self.cf(CF_ROA_CACHE_PROJECTION)?;
let key = roa_cache_projection_key(manifest_rsync_uri);
let Some(bytes) = self
.db
.get_cf(cf, key.as_bytes())
.map_err(|e| StorageError::RocksDb(e.to_string()))?
else {
return Ok(None);
};
let projection = decode_cbor::<RoaCacheProjection>(&bytes, "roa_cache_projection")?;
projection.validate_internal()?;
Ok(Some(projection))
}
pub fn put_transport_prefetch_snapshot(
&self,
snapshot: &crate::parallel::transport_prefetch::TransportPrefetchSnapshot,
) -> StorageResult<()> {
let cf = self.cf(CF_TRANSPORT_PREFETCH)?;
let value = encode_cbor(snapshot, "transport_prefetch_snapshot")?;
self.db
.put_cf(cf, TRANSPORT_PREFETCH_LAST_SNAPSHOT_KEY.as_bytes(), value)
.map_err(|e| StorageError::RocksDb(e.to_string()))
}
pub fn get_transport_prefetch_snapshot(
&self,
) -> StorageResult<Option<crate::parallel::transport_prefetch::TransportPrefetchSnapshot>> {
let cf = self.cf(CF_TRANSPORT_PREFETCH)?;
let Some(bytes) = self
.db
.get_cf(cf, TRANSPORT_PREFETCH_LAST_SNAPSHOT_KEY.as_bytes())
.map_err(|e| StorageError::RocksDb(e.to_string()))?
else {
return Ok(None);
};
decode_cbor::<crate::parallel::transport_prefetch::TransportPrefetchSnapshot>(
&bytes,
"transport_prefetch_snapshot",
)
.map(Some)
}
pub fn list_vcirs(&self) -> StorageResult<Vec<ValidatedCaInstanceResult>> {
let cf = self.cf(CF_VCIR)?;
let mode = IteratorMode::Start;
@ -2130,11 +2433,14 @@ impl RocksStore {
pub fn delete_vcir(&self, manifest_rsync_uri: &str) -> StorageResult<()> {
let vcir_cf = self.cf(CF_VCIR)?;
let replay_cf = self.cf(CF_MANIFEST_REPLAY_META)?;
let projection_cf = self.cf(CF_ROA_CACHE_PROJECTION)?;
let mut batch = WriteBatch::default();
let key = vcir_key(manifest_rsync_uri);
batch.delete_cf(vcir_cf, key.as_bytes());
let replay_key = manifest_replay_meta_key(manifest_rsync_uri);
batch.delete_cf(replay_cf, replay_key.as_bytes());
let projection_key = roa_cache_projection_key(manifest_rsync_uri);
batch.delete_cf(projection_cf, projection_key.as_bytes());
self.write_batch(batch)
}

View File

@ -5,9 +5,11 @@ pub const CF_RAW_BY_HASH: &str = "raw_by_hash";
pub const CF_RAW_BLOB: &str = "raw_blob";
pub const CF_VCIR: &str = "vcir";
pub const CF_MANIFEST_REPLAY_META: &str = "manifest_replay_meta";
pub const CF_ROA_CACHE_PROJECTION: &str = "roa_cache_projection";
pub const CF_RRDP_SOURCE: &str = "rrdp_source";
pub const CF_RRDP_SOURCE_MEMBER: &str = "rrdp_source_member";
pub const CF_RRDP_URI_OWNER: &str = "rrdp_uri_owner";
pub const CF_TRANSPORT_PREFETCH: &str = "transport_prefetch";
pub const ALL_COLUMN_FAMILY_NAMES: &[&str] = &[
CF_REPOSITORY_VIEW,
@ -15,9 +17,11 @@ pub const ALL_COLUMN_FAMILY_NAMES: &[&str] = &[
CF_RAW_BLOB,
CF_VCIR,
CF_MANIFEST_REPLAY_META,
CF_ROA_CACHE_PROJECTION,
CF_RRDP_SOURCE,
CF_RRDP_SOURCE_MEMBER,
CF_RRDP_URI_OWNER,
CF_TRANSPORT_PREFETCH,
];
pub(super) const REPOSITORY_VIEW_KEY_PREFIX: &str = "repo_view:";
@ -25,9 +29,11 @@ pub(super) const RAW_BY_HASH_KEY_PREFIX: &str = "rawbyhash:";
pub(super) const RAW_BLOB_KEY_PREFIX: &str = "rawblob:";
pub(super) const VCIR_KEY_PREFIX: &str = "vcir:";
pub(super) const MANIFEST_REPLAY_META_KEY_PREFIX: &str = "manifest_replay_meta:";
pub(super) const ROA_CACHE_PROJECTION_KEY_PREFIX: &str = "roa_cache_projection:";
pub(super) const RRDP_SOURCE_KEY_PREFIX: &str = "rrdp_source:";
pub(super) const RRDP_SOURCE_MEMBER_KEY_PREFIX: &str = "rrdp_source_member:";
pub(super) const RRDP_URI_OWNER_KEY_PREFIX: &str = "rrdp_uri_owner:";
pub(super) const TRANSPORT_PREFETCH_LAST_SNAPSHOT_KEY: &str = "transport_prefetch:last_snapshot";
const WORK_DB_BLOB_MODE_ENV: &str = "RPKI_WORK_DB_BLOB_MODE";
const WORK_DB_MEMORY_PROFILE_ENV: &str = "RPKI_WORK_DB_MEMORY_PROFILE";

View File

@ -30,6 +30,10 @@ pub(super) fn manifest_replay_meta_key(manifest_rsync_uri: &str) -> String {
format!("{MANIFEST_REPLAY_META_KEY_PREFIX}{manifest_rsync_uri}")
}
pub(super) fn roa_cache_projection_key(manifest_rsync_uri: &str) -> String {
format!("{ROA_CACHE_PROJECTION_KEY_PREFIX}{manifest_rsync_uri}")
}
pub(super) fn rrdp_source_key(notify_uri: &str) -> String {
format!("{RRDP_SOURCE_KEY_PREFIX}{notify_uri}")
}

View File

@ -298,6 +298,61 @@ fn vcir_ccr_manifest_projection_validate_rejects_invalid_fields() {
));
}
#[test]
fn roa_cache_projection_from_vcir_keeps_only_roa_vrp_outputs() {
let vcir = sample_vcir("rsync://example.test/repo/current.mft");
let projection = RoaCacheProjection::from_vcir(&vcir)
.expect("projection build")
.expect("projection exists");
assert_eq!(projection.manifest_rsync_uri, vcir.manifest_rsync_uri);
assert_eq!(projection.instance_effective_until, pack_time(12));
assert_eq!(projection.crl_sha256_by_uri.len(), 1);
assert_eq!(projection.entries.len(), 1);
assert_eq!(
projection.entries[0].source_object_uri,
"rsync://example.test/repo/object.roa"
);
assert_eq!(projection.entries[0].outputs.len(), 1);
assert!(matches!(
projection.entries[0].outputs[0].payload,
VcirLocalOutputPayload::Vrp { .. }
));
}
#[test]
fn roa_cache_projection_groups_multiple_outputs_by_roa_uri() {
let mut vcir = sample_vcir("rsync://example.test/repo/current.mft");
let mut second = vcir.local_outputs[0].clone();
second.rule_hash = sha256_32(b"vrp-rule-2");
if let VcirLocalOutputPayload::Vrp { max_length, .. } = &mut second.payload {
*max_length = 25;
}
vcir.local_outputs.push(second);
vcir.summary.local_vrp_count = 2;
let projection = RoaCacheProjection::from_vcir(&vcir)
.expect("projection build")
.expect("projection exists");
assert_eq!(projection.entries.len(), 1);
assert_eq!(projection.entries[0].outputs.len(), 2);
}
#[test]
fn roa_cache_projection_rejects_duplicate_uri_with_different_hash() {
let mut vcir = sample_vcir("rsync://example.test/repo/current.mft");
let mut duplicate = vcir.local_outputs[0].clone();
duplicate.source_object_hash = sha256_32(b"different-roa");
duplicate.rule_hash = sha256_32(b"vrp-rule-2");
vcir.local_outputs.push(duplicate);
vcir.summary.local_vrp_count = 2;
let err = RoaCacheProjection::from_vcir(&vcir)
.expect_err("same ROA URI with different hash must fail");
assert!(err.to_string().contains("source object hash mismatch"));
}
fn sample_rrdp_source_record(notify_uri: &str) -> RrdpSourceRecord {
RrdpSourceRecord {
notify_uri: notify_uri.to_string(),
@ -777,6 +832,16 @@ fn vcir_roundtrip_and_validation_failures_are_reported() {
updated_at_validation_time: vcir.last_successful_validation_time.clone(),
}
);
let projection = store
.get_roa_cache_projection(&vcir.manifest_rsync_uri)
.expect("get roa cache projection")
.expect("roa cache projection exists");
assert_eq!(projection.manifest_rsync_uri, vcir.manifest_rsync_uri);
assert_eq!(projection.entries.len(), 1);
assert_eq!(
projection.entries[0].source_object_uri,
"rsync://example.test/repo/object.roa"
);
let mut invalid = sample_vcir("rsync://example.test/repo/invalid.mft");
invalid.summary.local_vrp_count = 9;
@ -807,6 +872,65 @@ fn vcir_roundtrip_and_validation_failures_are_reported() {
.expect("get deleted manifest replay meta")
.is_none()
);
assert!(
store
.get_roa_cache_projection(&vcir.manifest_rsync_uri)
.expect("get deleted roa cache projection")
.is_none()
);
}
#[test]
fn transport_prefetch_snapshot_roundtrips() {
use crate::parallel::transport_prefetch::{
TransportPrefetchDedupKey, TransportPrefetchMode, TransportPrefetchRepoIdentity,
TransportPrefetchRequest, TransportPrefetchRequester, TransportPrefetchSnapshot,
};
use crate::policy::SyncPreference;
let td = tempfile::tempdir().expect("tempdir");
let store = RocksStore::open(td.path()).expect("open rocksdb");
assert!(
store
.get_transport_prefetch_snapshot()
.expect("get empty prefetch snapshot")
.is_none()
);
let snapshot = TransportPrefetchSnapshot::new(
SyncPreference::RrdpThenRsync,
vec![TransportPrefetchRequest {
dedup_key: TransportPrefetchDedupKey::RrdpNotify {
notification_uri: "https://example.test/notification.xml".to_string(),
},
rsync_scope_uri: "rsync://example.test/repo/".to_string(),
rsync_failure_scope_uri: Some("rsync://example.test/".to_string()),
repo_identity: TransportPrefetchRepoIdentity {
notification_uri: Some("https://example.test/notification.xml".to_string()),
rsync_base_uri: "rsync://example.test/repo/".to_string(),
},
mode: TransportPrefetchMode::Rrdp,
tal_id: "apnic".to_string(),
rir_id: "apnic".to_string(),
priority: 0,
requesters: vec![TransportPrefetchRequester {
tal_id: "apnic".to_string(),
rir_id: "apnic".to_string(),
parent_node_id: None,
ca_instance_handle_id: "apnic:rsync://example.test/repo/root.mft".to_string(),
publication_point_rsync_uri: "rsync://example.test/repo/".to_string(),
manifest_rsync_uri: "rsync://example.test/repo/root.mft".to_string(),
}],
}],
);
store
.put_transport_prefetch_snapshot(&snapshot)
.expect("put prefetch snapshot");
let got = store
.get_transport_prefetch_snapshot()
.expect("get prefetch snapshot")
.expect("snapshot exists");
assert_eq!(got, snapshot);
}
#[test]
@ -925,9 +1049,18 @@ fn replace_vcir_and_manifest_replay_meta_replaces_current_entry() {
.expect("store previous vcir");
assert!(previous_timing.vcir_value_bytes > 0);
assert!(previous_timing.replay_meta_value_bytes > 0);
assert!(previous_timing.roa_cache_projection_value_bytes > 0);
assert_eq!(
previous_timing.total_encoded_bytes,
previous_timing.vcir_value_bytes + previous_timing.replay_meta_value_bytes
previous_timing.vcir_value_bytes
+ previous_timing.replay_meta_value_bytes
+ previous_timing.roa_cache_projection_value_bytes
);
assert!(
store
.get_roa_cache_projection(&previous.manifest_rsync_uri)
.expect("get previous projection")
.is_some()
);
let mut current = sample_vcir("rsync://example.test/repo/current.mft");
@ -951,6 +1084,7 @@ fn replace_vcir_and_manifest_replay_meta_replaces_current_entry() {
.expect("replace vcir and replay meta");
assert!(current_timing.vcir_value_bytes > 0);
assert!(current_timing.replay_meta_value_bytes > 0);
assert_eq!(current_timing.roa_cache_projection_value_bytes, 0);
assert_eq!(
current_timing.total_encoded_bytes,
current_timing.vcir_value_bytes + current_timing.replay_meta_value_bytes
@ -973,6 +1107,12 @@ fn replace_vcir_and_manifest_replay_meta_replaces_current_entry() {
replay_meta.manifest_sha256,
current.ccr_manifest_projection.manifest_sha256
);
assert!(
store
.get_roa_cache_projection(&current.manifest_rsync_uri)
.expect("get current projection")
.is_none()
);
}
#[test]

View File

@ -15,9 +15,9 @@ use crate::parallel::object_worker::{
use crate::policy::{Policy, SignedObjectFailurePolicy};
use crate::report::{RfcRef, Warning};
use crate::storage::{
PackFile, PackTime, ValidatedCaInstanceResult, VcirArtifactKind, VcirArtifactRole,
VcirArtifactValidationStatus, VcirLocalOutput, VcirLocalOutputPayload, VcirOutputType,
VcirSourceObjectType,
PackFile, PackTime, RoaCacheProjection, ValidatedCaInstanceResult, VcirArtifactKind,
VcirArtifactRole, VcirArtifactValidationStatus, VcirLocalOutput, VcirLocalOutputPayload,
VcirOutputType, VcirSourceObjectType,
};
use crate::validation::cert_path::{CertPathError, validate_signed_object_ee_cert_path_fast};
use crate::validation::manifest::PublicationPointData;
@ -264,6 +264,65 @@ pub struct CachedRoaValidationResult {
}
impl RoaValidationCacheView {
pub fn from_projection(
projection: &RoaCacheProjection,
validation_time: time::OffsetDateTime,
) -> Self {
let mut entries_by_uri: HashMap<String, CachedRoaValidationResult> =
HashMap::with_capacity(projection.entries.len());
let issuer_ca_sha256_hex = projection.issuer_ca_sha256_hex.clone();
let crl_sha256_by_uri = projection
.crl_sha256_by_uri
.iter()
.map(|crl| (crl.uri.clone(), crl.sha256.clone()))
.collect::<HashMap<_, _>>();
let blocked = projection
.instance_effective_until
.parse()
.map(|effective_until| effective_until <= validation_time)
.unwrap_or(true);
if blocked {
return Self {
entries_by_uri,
issuer_ca_sha256_hex,
crl_sha256_by_uri,
blocked,
};
}
for entry in &projection.entries {
let outputs = entry
.outputs
.iter()
.map(|output| VcirLocalOutput {
output_type: VcirOutputType::Vrp,
item_effective_until: output.item_effective_until.clone(),
source_object_uri: entry.source_object_uri.clone(),
source_object_type: VcirSourceObjectType::Roa,
source_object_hash: entry.source_object_hash,
source_ee_cert_hash: output.source_ee_cert_hash,
payload: output.payload.clone(),
rule_hash: output.rule_hash,
})
.collect::<Vec<_>>();
entries_by_uri.insert(
entry.source_object_uri.clone(),
CachedRoaValidationResult {
source_object_hash: entry.source_object_hash,
outputs,
},
);
}
Self {
entries_by_uri,
issuer_ca_sha256_hex,
crl_sha256_by_uri,
blocked,
}
}
pub fn from_vcir(
vcir: &ValidatedCaInstanceResult,
validation_time: time::OffsetDateTime,
@ -3067,8 +3126,8 @@ mod tests {
};
use crate::policy::Policy;
use crate::storage::{
PackTime, ValidatedManifestMeta, VcirAuditSummary, VcirCcrManifestProjection,
VcirInstanceGate, VcirRelatedArtifact, VcirSummary,
PackTime, RoaCacheProjection, ValidatedManifestMeta, VcirAuditSummary,
VcirCcrManifestProjection, VcirInstanceGate, VcirRelatedArtifact, VcirSummary,
};
use crate::validation::publication_point::PublicationPointSnapshot;
use std::collections::HashMap;
@ -3214,6 +3273,120 @@ mod tests {
assert_eq!(ok.local_outputs.len(), 1);
}
#[test]
fn roa_validation_cache_view_from_projection_matches_vcir_view() {
let validation_time = fixed_time("2026-06-05T00:00:00Z");
let issuer_der = b"issuer-ca";
let crl_hash = [0x22; 32];
let roa_hash = [0x11; 32];
let vcir = sample_roa_cache_vcir(
issuer_der,
crl_hash,
roa_hash,
fixed_time("2026-06-07T00:00:00Z"),
fixed_time("2026-06-08T00:00:00Z"),
);
let projection = RoaCacheProjection::from_vcir(&vcir)
.expect("build projection")
.expect("projection exists");
let view = RoaValidationCacheView::from_projection(&projection, validation_time);
let files = vec![
PackFile::from_bytes_with_sha256(
"rsync://example.test/repo/a.roa",
vec![0x01],
roa_hash,
),
PackFile::from_bytes_with_sha256(
"rsync://example.test/repo/current.crl",
vec![0x02],
crl_hash,
),
];
assert!(view.matches_current_context(issuer_der, &files));
let hit = view.lookup(&files[0], validation_time);
let RoaCacheLookupResult::Hit(ok) = hit else {
panic!("expected projection cache hit, got {hit:?}");
};
assert!(ok.reused_from_cache);
assert_eq!(ok.vrps.len(), 1);
assert_eq!(ok.vrps[0].asn, 64500);
assert_eq!(ok.local_outputs.len(), 1);
assert_eq!(
ok.local_outputs[0].source_object_uri,
"rsync://example.test/repo/a.roa"
);
}
#[test]
fn roa_validation_cache_view_from_projection_blocks_on_gates() {
let validation_time = fixed_time("2026-06-05T00:00:00Z");
let issuer_der = b"issuer-ca";
let crl_hash = [0x22; 32];
let roa_hash = [0x11; 32];
let vcir = sample_roa_cache_vcir(
issuer_der,
crl_hash,
roa_hash,
fixed_time("2026-06-07T00:00:00Z"),
fixed_time("2026-06-08T00:00:00Z"),
);
let mut projection = RoaCacheProjection::from_vcir(&vcir)
.expect("build projection")
.expect("projection exists");
let files = vec![
PackFile::from_bytes_with_sha256(
"rsync://example.test/repo/a.roa",
vec![0x01],
roa_hash,
),
PackFile::from_bytes_with_sha256(
"rsync://example.test/repo/current.crl",
vec![0x02],
crl_hash,
),
];
projection.issuer_ca_sha256_hex = Some("00".repeat(32));
let issuer_changed = RoaValidationCacheView::from_projection(&projection, validation_time);
assert!(!issuer_changed.matches_current_context(issuer_der, &files));
let mut projection = RoaCacheProjection::from_vcir(&vcir)
.expect("build projection")
.expect("projection exists");
projection.crl_sha256_by_uri[0].sha256 = "11".repeat(32);
let crl_changed = RoaValidationCacheView::from_projection(&projection, validation_time);
assert!(!crl_changed.matches_current_context(issuer_der, &files));
let mut projection = RoaCacheProjection::from_vcir(&vcir)
.expect("build projection")
.expect("projection exists");
projection.entries[0].source_object_hash = [0xff; 32];
let roa_changed = RoaValidationCacheView::from_projection(&projection, validation_time);
assert!(matches!(
roa_changed.lookup(&files[0], validation_time),
RoaCacheLookupResult::Blocked
));
let expired_vcir = sample_roa_cache_vcir(
issuer_der,
crl_hash,
roa_hash,
fixed_time("2026-06-07T00:00:00Z"),
fixed_time("2026-06-04T00:00:00Z"),
);
let expired_projection = RoaCacheProjection::from_vcir(&expired_vcir)
.expect("build projection")
.expect("projection exists");
let expired_view =
RoaValidationCacheView::from_projection(&expired_projection, validation_time);
assert!(!expired_view.matches_current_context(issuer_der, &files));
assert!(matches!(
expired_view.lookup(&files[0], validation_time),
RoaCacheLookupResult::Blocked
));
}
#[test]
fn roa_validation_cache_view_blocks_when_context_changes() {
let validation_time = fixed_time("2026-06-05T00:00:00Z");

View File

@ -13,6 +13,7 @@ use crate::parallel::repo_worker::{
};
use crate::parallel::run_coordinator::GlobalRunCoordinator;
use crate::parallel::types::{TalInputSpec, TalSource};
use crate::policy::SyncPreference;
use crate::replay::archive::ReplayArchiveIndex;
use crate::replay::delta_archive::ReplayDeltaArchiveIndex;
use crate::replay::delta_fetch_http::PayloadDeltaReplayHttpFetcher;
@ -171,6 +172,7 @@ fn build_phase1_repo_sync_runtime<H, R>(
timing: Option<TimingHandle>,
download_log: Option<DownloadLogHandle>,
tal_inputs: Vec<crate::parallel::types::TalInputSpec>,
record_transport_prefetch_requests: bool,
) -> Result<(Arc<dyn RepoSyncRuntime>, CurrentRepoIndexHandle), RunTreeFromTalError>
where
H: Fetcher + Clone + 'static,
@ -184,7 +186,7 @@ where
current_repo_index.clone(),
Arc::new(http_fetcher.clone()),
Arc::clone(&rsync_fetcher_arc),
timing,
timing.clone(),
download_log,
);
let pool = RepoTransportWorkerPool::new(RepoWorkerPoolConfig::from(&parallel_config), executor)
@ -196,16 +198,142 @@ where
let failure_resolver: Arc<dyn Fn(&str) -> Option<String> + Send + Sync> =
Arc::new(move |base: &str| failure_rsync_fetcher_arc.failure_dedup_key(base));
let _ = policy; // policy reserved for later runtime-level decisions
let runtime = Arc::new(Phase1RepoSyncRuntime::new_with_failure_scope(
coordinator,
pool,
resolver,
failure_resolver,
policy.sync_preference,
));
let runtime = Arc::new(
Phase1RepoSyncRuntime::new_with_failure_scope_and_prefetch_recording(
coordinator,
pool,
resolver,
failure_resolver,
policy.sync_preference,
record_transport_prefetch_requests,
),
);
Ok((runtime, current_repo_index))
}
fn record_transport_prefetch_count(timing: Option<&TimingHandle>, key: &'static str, value: u64) {
if value == 0 {
return;
}
if let Some(timing) = timing {
timing.record_count(key, value);
}
}
fn apply_transport_request_prefetch(
store: &crate::storage::RocksStore,
runtime: &Arc<dyn RepoSyncRuntime>,
sync_preference: SyncPreference,
validation_time: time::OffsetDateTime,
config: &TreeRunConfig,
timing: Option<&TimingHandle>,
) -> Result<(), RunTreeFromTalError> {
if !config.enable_transport_request_prefetch {
return Ok(());
}
let snapshot = match store.get_transport_prefetch_snapshot() {
Ok(Some(snapshot)) => snapshot,
Ok(None) => {
crate::progress_log::emit(
"phase1_repo_prefetch_missing_snapshot",
serde_json::json!({ "status": "missing" }),
);
return Ok(());
}
Err(err) => {
record_transport_prefetch_count(timing, "transport_prefetch_load_errors", 1);
crate::progress_log::emit(
"phase1_repo_prefetch_load_error",
serde_json::json!({ "error": err.to_string() }),
);
return Ok(());
}
};
if !snapshot.is_compatible_with(sync_preference) {
record_transport_prefetch_count(
timing,
"transport_prefetch_skipped_incompatible_snapshots",
1,
);
crate::progress_log::emit(
"phase1_repo_prefetch_skipped",
serde_json::json!({
"reason": "incompatible_snapshot",
"schema_version": snapshot.schema_version,
"request_count": snapshot.requests.len(),
}),
);
return Ok(());
}
let stats = runtime
.prefetch_transport_requests(&snapshot, validation_time)
.map_err(TreeRunError::Runner)?;
record_transport_prefetch_count(
timing.clone(),
"transport_prefetch_loaded_requests",
stats.loaded_requests,
);
record_transport_prefetch_count(
timing.clone(),
"transport_prefetch_enqueued_tasks",
stats.enqueued_tasks,
);
record_transport_prefetch_count(
timing,
"transport_prefetch_waiting_requests",
stats.waiting_requests,
);
record_transport_prefetch_count(
timing,
"transport_prefetch_reused_results",
stats.reused_results,
);
record_transport_prefetch_count(
timing,
"transport_prefetch_skipped_incompatible_requests",
stats.skipped_incompatible,
);
crate::progress_log::emit(
"phase1_repo_prefetch_applied",
serde_json::json!({
"loaded_requests": stats.loaded_requests,
"enqueued_tasks": stats.enqueued_tasks,
"waiting_requests": stats.waiting_requests,
"reused_results": stats.reused_results,
"skipped_incompatible": stats.skipped_incompatible,
}),
);
Ok(())
}
fn persist_transport_request_prefetch_snapshot(
store: &crate::storage::RocksStore,
runtime: &Arc<dyn RepoSyncRuntime>,
config: &TreeRunConfig,
timing: Option<&TimingHandle>,
) -> Result<(), RunTreeFromTalError> {
if !config.enable_transport_request_prefetch {
return Ok(());
}
let snapshot = runtime.transport_prefetch_snapshot();
let recorded = snapshot.requests.len() as u64;
store
.put_transport_prefetch_snapshot(&snapshot)
.map_err(|err| TreeRunError::Runner(err.to_string()))?;
record_transport_prefetch_count(timing, "transport_prefetch_recorded_requests", recorded);
crate::progress_log::emit(
"phase1_repo_prefetch_persisted",
serde_json::json!({
"recorded_requests": recorded,
"schema_version": snapshot.schema_version,
}),
);
Ok(())
}
fn root_discovery_from_tal_input(
tal_input: &TalInputSpec,
http_fetcher: &dyn Fetcher,
@ -588,6 +716,15 @@ where
timing.clone(),
Some(download_log.clone()),
tal_inputs,
config.enable_transport_request_prefetch,
)?;
apply_transport_request_prefetch(
store.as_ref(),
&runtime,
policy.sync_preference,
validation_time,
config,
timing.as_ref(),
)?;
let current_repo_index_for_output = current_repo_index.clone();
let runner = make_live_runner(
@ -596,10 +733,10 @@ where
http_fetcher,
rsync_fetcher,
validation_time,
timing,
timing.clone(),
Some(download_log.clone()),
Some(current_repo_index),
Some(runtime),
Some(Arc::clone(&runtime)),
phase2_config,
(phase2_enabled && config.build_ccr_accumulator)
.then(|| CcrAccumulator::new(vec![discovery.trust_anchor.clone()])),
@ -622,6 +759,7 @@ where
} else {
run_tree_serial_audit(root, &runner, config)?
};
persist_transport_request_prefetch_snapshot(store.as_ref(), &runtime, config, timing.as_ref())?;
let downloads = download_log.snapshot_events();
let download_stats = DownloadLogHandle::stats_from_events(&downloads);
Ok(RunTreeFromTalAuditOutput {
@ -696,6 +834,15 @@ where
timing.clone(),
Some(download_log.clone()),
successful_tal_inputs.clone(),
config.enable_transport_request_prefetch,
)?;
apply_transport_request_prefetch(
store.as_ref(),
&runtime,
policy.sync_preference,
validation_time,
config,
timing.as_ref(),
)?;
let current_repo_index_for_output = current_repo_index.clone();
let runner = make_live_runner(
@ -704,10 +851,10 @@ where
http_fetcher,
rsync_fetcher,
validation_time,
timing,
timing.clone(),
Some(download_log.clone()),
Some(current_repo_index),
Some(runtime),
Some(Arc::clone(&runtime)),
phase2_config,
(phase2_enabled && config.build_ccr_accumulator).then(|| {
CcrAccumulator::new(
@ -730,6 +877,7 @@ where
} else {
run_tree_serial_audit_multi_root(root_handles, &runner, config)?
};
persist_transport_request_prefetch_snapshot(store.as_ref(), &runtime, config, timing.as_ref())?;
let downloads = download_log.snapshot_events();
let download_stats = DownloadLogHandle::stats_from_events(&downloads);
Ok(RunTreeFromTalAuditOutput {
@ -2329,6 +2477,7 @@ mod replay_api_tests {
persist_vcir: true,
build_ccr_accumulator: true,
enable_roa_validation_cache: false,
enable_transport_request_prefetch: false,
},
)
.unwrap_err();
@ -2365,6 +2514,7 @@ mod replay_api_tests {
persist_vcir: true,
build_ccr_accumulator: true,
enable_roa_validation_cache: false,
enable_transport_request_prefetch: false,
},
)
.expect("run replay root-only audit");
@ -2411,6 +2561,7 @@ mod replay_api_tests {
persist_vcir: true,
build_ccr_accumulator: true,
enable_roa_validation_cache: false,
enable_transport_request_prefetch: false,
},
)
.expect("run replay root-only audit");
@ -2458,6 +2609,7 @@ mod replay_api_tests {
persist_vcir: true,
build_ccr_accumulator: true,
enable_roa_validation_cache: false,
enable_transport_request_prefetch: false,
},
&timing,
)
@ -2515,6 +2667,7 @@ mod replay_api_tests {
persist_vcir: true,
build_ccr_accumulator: true,
enable_roa_validation_cache: false,
enable_transport_request_prefetch: false,
},
)
.unwrap_err();
@ -2548,6 +2701,7 @@ mod replay_api_tests {
persist_vcir: true,
build_ccr_accumulator: true,
enable_roa_validation_cache: false,
enable_transport_request_prefetch: false,
},
)
.unwrap_err();
@ -2601,6 +2755,7 @@ mod replay_api_tests {
persist_vcir: true,
build_ccr_accumulator: true,
enable_roa_validation_cache: false,
enable_transport_request_prefetch: false,
},
)
.expect("run delta replay root-only audit");
@ -2663,6 +2818,7 @@ mod replay_api_tests {
persist_vcir: true,
build_ccr_accumulator: true,
enable_roa_validation_cache: false,
enable_transport_request_prefetch: false,
},
&timing,
)

View File

@ -22,6 +22,8 @@ pub struct TreeRunConfig {
pub build_ccr_accumulator: bool,
/// Reuse accepted ROA validation outputs from previous VCIR when explicitly enabled.
pub enable_roa_validation_cache: bool,
/// Prefetch transport requests from the previous run before normal tree traversal.
pub enable_transport_request_prefetch: bool,
}
impl Default for TreeRunConfig {
@ -33,6 +35,7 @@ impl Default for TreeRunConfig {
persist_vcir: true,
build_ccr_accumulator: true,
enable_roa_validation_cache: false,
enable_transport_request_prefetch: false,
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -113,6 +113,8 @@ pub(crate) struct FreshPublicationPointFinalizeOutput {
pub(crate) snapshot_pack_ms: u64,
pub(crate) persist_vcir_ms: u64,
pub(crate) persist_vcir_timing: PersistVcirTimingBreakdown,
pub(crate) ccr_projection_build_ms: u64,
pub(crate) ccr_append_ms: u64,
pub(crate) audit_build_ms: u64,
}
@ -163,20 +165,25 @@ impl<'a> Rpkiv1PublicationPointRunner<'a> {
return None;
}
let load_started = std::time::Instant::now();
let loaded_vcir = self.store.get_vcir(manifest_rsync_uri);
let loaded_projection = self.store.get_roa_cache_projection(manifest_rsync_uri);
if let Some(timing) = self.timing.as_ref() {
timing.record_phase_nanos(
"roa_validation_cache_vcir_load_total",
"roa_validation_cache_projection_load_total",
load_started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64,
);
}
match loaded_vcir {
Ok(Some(vcir)) => {
match loaded_projection {
Ok(Some(projection)) => {
if let Some(timing) = self.timing.as_ref() {
timing
.record_count("roa_validation_cache_projection_hit_publication_points", 1);
}
let view_started = std::time::Instant::now();
let view = RoaValidationCacheView::from_vcir(&vcir, self.validation_time);
let view =
RoaValidationCacheView::from_projection(&projection, self.validation_time);
if let Some(timing) = self.timing.as_ref() {
timing.record_phase_nanos(
"roa_validation_cache_view_build_total",
"roa_validation_cache_projection_build_total",
view_started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64,
);
}
@ -184,16 +191,19 @@ impl<'a> Rpkiv1PublicationPointRunner<'a> {
}
Ok(None) => {
if let Some(timing) = self.timing.as_ref() {
timing.record_count("roa_validation_cache_vcir_missing_publication_points", 1);
timing.record_count(
"roa_validation_cache_projection_missing_publication_points",
1,
);
}
None
}
Err(err) => {
if let Some(timing) = self.timing.as_ref() {
timing.record_count("roa_validation_cache_vcir_load_errors", 1);
timing.record_count("roa_validation_cache_projection_load_errors", 1);
}
crate::progress_log::emit(
"roa_validation_cache_vcir_load_error",
"roa_validation_cache_projection_load_error",
serde_json::json!({
"manifest_rsync_uri": manifest_rsync_uri,
"error": err.to_string(),
@ -381,12 +391,18 @@ impl<'a> Rpkiv1PublicationPointRunner<'a> {
// publication point result is retained for the rest of the run.
let _released_local_outputs = std::mem::take(&mut objects.local_outputs_cache);
let mut ccr_projection_build_ms = 0;
let mut ccr_append_ms = 0;
if self.ccr_accumulator.is_some() {
let ccr_projection_build_started = std::time::Instant::now();
let child_entries =
build_vcir_child_entries(&discovered_children, self.validation_time)?;
let ccr_manifest_projection =
build_vcir_ccr_manifest_projection_from_fresh(ca, &pack, &child_entries)?;
ccr_projection_build_ms = ccr_projection_build_started.elapsed().as_millis() as u64;
let ccr_append_started = std::time::Instant::now();
self.append_ccr_manifest_projection(&ccr_manifest_projection)?;
ccr_append_ms = ccr_append_started.elapsed().as_millis() as u64;
}
let audit_build_started = std::time::Instant::now();
@ -416,6 +432,8 @@ impl<'a> Rpkiv1PublicationPointRunner<'a> {
snapshot_pack_ms,
persist_vcir_ms,
persist_vcir_timing,
ccr_projection_build_ms,
ccr_append_ms,
audit_build_ms,
})
}
@ -805,6 +823,8 @@ impl<'a> PublicationPointRunner for Rpkiv1PublicationPointRunner<'a> {
snapshot_pack_ms,
persist_vcir_ms,
persist_vcir_timing,
ccr_projection_build_ms,
ccr_append_ms,
audit_build_ms,
} = finalized;
let total_duration_ms = publication_point_started.elapsed().as_millis() as u64;
@ -841,6 +861,8 @@ impl<'a> PublicationPointRunner for Rpkiv1PublicationPointRunner<'a> {
"persist_related_artifacts_ms": persist_vcir_timing.build_vcir.related_artifacts_ms,
"persist_vcir_struct_ms": persist_vcir_timing.build_vcir.struct_build_ms,
"persist_replace_breakdown": &persist_vcir_timing.replace_vcir,
"ccr_projection_build_ms": ccr_projection_build_ms,
"ccr_append_ms": ccr_append_ms,
"audit_build_ms": audit_build_ms,
"warning_count": result.warnings.len(),
"vrp_count": result.objects.vrps.len(),
@ -884,6 +906,8 @@ impl<'a> PublicationPointRunner for Rpkiv1PublicationPointRunner<'a> {
"persist_related_artifacts_ms": persist_vcir_timing.build_vcir.related_artifacts_ms,
"persist_vcir_struct_ms": persist_vcir_timing.build_vcir.struct_build_ms,
"persist_replace_breakdown": &persist_vcir_timing.replace_vcir,
"ccr_projection_build_ms": ccr_projection_build_ms,
"ccr_append_ms": ccr_append_ms,
"audit_build_ms": audit_build_ms,
}),
);

View File

@ -3034,6 +3034,85 @@ fn parse_snapshot_time_value_reports_invalid_timestamp() {
assert!(err.contains("invalid RFC3339 time 'not-a-time'"), "{err}");
}
#[test]
fn runner_roa_validation_cache_uses_projection_not_full_vcir_fallback() {
let now = time::OffsetDateTime::now_utc();
let child_cert_hash = sha256_hex(b"child-cert");
let mut vcir = sample_vcir_for_projection(now, &child_cert_hash);
vcir.local_outputs
.retain(|output| output.source_object_type != VcirSourceObjectType::Roa);
vcir.summary.local_vrp_count = 0;
vcir.summary.local_aspa_count = 1;
vcir.summary.local_router_key_count = 1;
let store_dir = tempfile::tempdir().expect("store dir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
store.put_vcir(&vcir).expect("put vcir without projection");
assert!(
store
.get_vcir(&vcir.manifest_rsync_uri)
.expect("get vcir")
.is_some()
);
assert!(
store
.get_roa_cache_projection(&vcir.manifest_rsync_uri)
.expect("get projection")
.is_none()
);
let timing = crate::analysis::timing::TimingHandle::new(crate::analysis::timing::TimingMeta {
recorded_at_utc_rfc3339: "2026-06-07T00:00:00Z".to_string(),
validation_time_utc_rfc3339: "2026-06-07T00:00:00Z".to_string(),
tal_url: None,
db_path: None,
});
let policy = Policy::default();
let runner = Rpkiv1PublicationPointRunner {
store: &store,
policy: &policy,
http_fetcher: &NeverHttpFetcher,
rsync_fetcher: &FailingRsyncFetcher,
validation_time: now,
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: true,
};
assert!(
runner
.roa_validation_cache_view_for_fresh_point(&vcir.manifest_rsync_uri)
.is_none()
);
let dir = tempfile::tempdir().expect("timing dir");
let path = dir.path().join("timing.json");
timing.write_json(&path, 10).expect("write timing");
let report: serde_json::Value =
serde_json::from_slice(&std::fs::read(path).expect("read timing")).expect("parse timing");
assert_eq!(
report["counts"]["roa_validation_cache_projection_missing_publication_points"],
1
);
assert!(
report["phases"]["roa_validation_cache_projection_load_total"]["count"]
.as_u64()
.unwrap_or_default()
>= 1
);
}
#[test]
fn build_objects_output_from_vcir_tracks_expired_and_invalid_cached_outputs() {
let now = time::OffsetDateTime::now_utc();

View File

@ -219,6 +219,7 @@ fn apnic_tree_full_stats_serial() {
persist_vcir: true,
build_ccr_accumulator: true,
enable_roa_validation_cache: false,
enable_transport_request_prefetch: false,
},
)
.expect("run tree");

View File

@ -40,6 +40,7 @@ fn apnic_tree_depth1_processes_more_than_root() {
persist_vcir: true,
build_ccr_accumulator: true,
enable_roa_validation_cache: false,
enable_transport_request_prefetch: false,
},
)
.expect("run tree from tal");
@ -82,6 +83,7 @@ fn apnic_tree_root_only_processes_root_with_long_timeouts() {
persist_vcir: true,
build_ccr_accumulator: true,
enable_roa_validation_cache: false,
enable_transport_request_prefetch: false,
},
)
.expect("run APNIC root-only");

View File

@ -112,6 +112,7 @@ fn crl_mismatch_drops_publication_point_and_cites_rfc_sections() {
persist_vcir: true,
build_ccr_accumulator: true,
enable_roa_validation_cache: false,
enable_transport_request_prefetch: false,
},
)
.expect("run tree audit");

View File

@ -118,6 +118,7 @@ fn run_tree_from_tal_url_entry_executes_and_records_failure_when_repo_empty() {
persist_vcir: true,
build_ccr_accumulator: true,
enable_roa_validation_cache: false,
enable_transport_request_prefetch: false,
},
)
.expect("run tree");
@ -166,6 +167,7 @@ fn run_tree_from_tal_and_ta_der_entry_executes_and_records_failure_when_repo_emp
persist_vcir: true,
build_ccr_accumulator: true,
enable_roa_validation_cache: false,
enable_transport_request_prefetch: false,
},
)
.expect("run tree");
@ -222,6 +224,7 @@ fn run_tree_from_tal_url_audit_entry_collects_no_publication_points_when_repo_em
persist_vcir: true,
build_ccr_accumulator: true,
enable_roa_validation_cache: false,
enable_transport_request_prefetch: false,
},
)
.expect("run tree audit");
@ -266,6 +269,7 @@ fn run_tree_from_tal_and_ta_der_audit_entry_collects_no_publication_points_when_
persist_vcir: true,
build_ccr_accumulator: true,
enable_roa_validation_cache: false,
enable_transport_request_prefetch: false,
},
)
.expect("run tree audit");
@ -318,6 +322,7 @@ fn run_tree_from_tal_url_audit_with_timing_records_phases_when_repo_empty() {
persist_vcir: true,
build_ccr_accumulator: true,
enable_roa_validation_cache: false,
enable_transport_request_prefetch: false,
},
&timing,
)
@ -369,6 +374,7 @@ fn run_tree_from_tal_and_ta_der_audit_with_timing_records_phases_when_repo_empty
persist_vcir: true,
build_ccr_accumulator: true,
enable_roa_validation_cache: false,
enable_transport_request_prefetch: false,
},
&timing,
)

View File

@ -296,6 +296,7 @@ fn tree_respects_max_depth_and_max_instances() {
persist_vcir: true,
build_ccr_accumulator: true,
enable_roa_validation_cache: false,
enable_transport_request_prefetch: false,
},
)
.expect("run tree depth-limited");
@ -312,6 +313,7 @@ fn tree_respects_max_depth_and_max_instances() {
persist_vcir: true,
build_ccr_accumulator: true,
enable_roa_validation_cache: false,
enable_transport_request_prefetch: false,
},
)
.expect("run tree instance-limited");