优化rtr

This commit is contained in:
xiuting.xu
2026-04-08 14:03:23 +08:00
parent b483c2af8b
commit 23cdad095d
16 changed files with 1835 additions and 1606 deletions

View File

@@ -59,6 +59,18 @@ fn snapshot_hashes_and_sorted_view_to_string(snapshot: &Snapshot) -> String {
)
}
fn serials_all(serial: u32) -> [u32; 3] {
[serial; 3]
}
fn snapshots_all(snapshot: Snapshot) -> [Snapshot; 3] {
[snapshot.clone(), snapshot.clone(), snapshot]
}
fn deltas_all(deltas: VecDeque<Arc<Delta>>) -> [VecDeque<Arc<Delta>>; 3] {
[deltas.clone(), deltas.clone(), deltas]
}
/// Snapshot ?hash ?
/// payload snapshot_hash / origins_hash ?
#[test]
@@ -127,8 +139,8 @@ async fn init_keeps_cache_running_when_file_loader_returns_no_data() {
.unwrap();
assert!(!cache.is_data_available());
assert_eq!(cache.serial(), 0);
assert!(cache.snapshot().payloads_for_rtr().is_empty());
assert_eq!(cache.serial_for_version(2), 0);
assert!(cache.snapshot_for_version(2).payloads_for_rtr().is_empty());
}
#[tokio::test]
@@ -162,37 +174,40 @@ async fn init_restores_wraparound_delta_window_from_store() {
vec![],
);
let snapshots = snapshots_all(snapshot.clone());
let session_ids = session_ids.as_array();
store
.save_cache_state(
.save_cache_state_versioned(
CacheAvailability::Ready,
&snapshot,
&snapshots,
&session_ids,
u32::MAX,
Some(&d_max),
Some((u32::MAX, u32::MAX)),
false,
&serials_all(u32::MAX),
&[None, None, Some(&d_max)],
&[None, None, Some((u32::MAX, u32::MAX))],
&[false, false, false],
)
.unwrap();
store
.save_cache_state(
.save_cache_state_versioned(
CacheAvailability::Ready,
&snapshot,
&snapshots,
&session_ids,
0,
Some(&d_zero),
Some((u32::MAX, 0)),
false,
&serials_all(0),
&[None, None, Some(&d_zero)],
&[None, None, Some((u32::MAX, 0))],
&[false, false, false],
)
.unwrap();
store
.save_cache_state(
.save_cache_state_versioned(
CacheAvailability::Ready,
&snapshot,
&snapshots,
&session_ids,
1,
Some(&d_one),
Some((u32::MAX, 1)),
false,
&serials_all(1),
&[None, None, Some(&d_one)],
&[None, None, Some((u32::MAX, 1))],
&[false, false, false],
)
.unwrap();
@@ -202,7 +217,7 @@ async fn init_restores_wraparound_delta_window_from_store() {
})
.unwrap();
match cache.get_deltas_since(u32::MAX.wrapping_sub(1)) {
match cache.get_deltas_since_for_version(2, u32::MAX.wrapping_sub(1)) {
SerialResult::Delta(delta) => {
assert_eq!(delta.serial(), 1);
assert_eq!(delta.announced().len(), 3);
@@ -211,6 +226,60 @@ async fn init_restores_wraparound_delta_window_from_store() {
}
}
#[tokio::test]
async fn init_restore_respects_max_delta_limit() {
let dir = tempfile::tempdir().unwrap();
let store = RtrStore::open(dir.path()).unwrap();
let session_ids = SessionIds::from_array([42, 43, 44]).as_array();
let a = Payload::RouteOrigin(v4_origin(192, 0, 2, 0, 24, 24, 64496));
let b = Payload::RouteOrigin(v4_origin(198, 51, 100, 0, 24, 24, 64497));
let snapshot = Snapshot::from_payloads(vec![a.clone(), b.clone()]);
let snapshots = snapshots_all(snapshot);
let d17 = Delta::new(17, vec![a], vec![]);
let d18 = Delta::new(18, vec![b], vec![]);
store
.save_cache_state_versioned(
CacheAvailability::Ready,
&snapshots,
&session_ids,
&serials_all(17),
&[None, None, Some(&d17)],
&[None, None, Some((17, 17))],
&[false, false, false],
)
.unwrap();
store
.save_cache_state_versioned(
CacheAvailability::Ready,
&snapshots,
&session_ids,
&serials_all(18),
&[None, None, Some(&d18)],
&[None, None, Some((17, 18))],
&[false, false, false],
)
.unwrap();
let cache = rpki::rtr::cache::RtrCache::default()
.init(&store, 1, false, Timing::new(600, 600, 7200), || {
Ok(Vec::new())
})
.unwrap();
match cache.get_deltas_since_for_version(2, 16) {
SerialResult::ResetRequired => {}
_ => panic!("expected ResetRequired for serial older than retained max_delta window"),
}
match cache.get_deltas_since_for_version(2, 17) {
SerialResult::Delta(delta) => assert_eq!(delta.serial(), 18),
_ => panic!("expected one retained delta from 17 -> 18"),
}
}
#[tokio::test]
async fn update_prunes_delta_window_when_cumulative_delta_size_reaches_snapshot_size() {
let dir = tempfile::tempdir().unwrap();
@@ -229,8 +298,8 @@ async fn update_prunes_delta_window_when_cumulative_delta_size_reaches_snapshot_
let mut cache = RtrCacheBuilder::new()
.availability(CacheAvailability::Ready)
.session_ids(SessionIds::from_array([42, 43, 44]))
.serial(1)
.snapshot(initial_snapshot)
.serials(serials_all(1))
.snapshots(snapshots_all(initial_snapshot))
.max_delta(16)
.prune_delta_by_snapshot_size(true)
.timing(Timing::new(600, 600, 7200))
@@ -247,7 +316,7 @@ async fn update_prunes_delta_window_when_cumulative_delta_size_reaches_snapshot_
)
.unwrap();
match cache.get_deltas_since(1) {
match cache.get_deltas_since_for_version(2, 1) {
SerialResult::ResetRequired => {}
_ => panic!(
"expected delta window to be pruned when cumulative delta size exceeds snapshot size"
@@ -444,14 +513,14 @@ fn delta_new_sorts_announced_descending_and_withdrawn_ascending() {
fn get_deltas_since_returns_up_to_date_when_client_serial_matches_current() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serial(100)
.serials(serials_all(100))
.timing(Timing::default())
.build();
let result = cache.get_deltas_since(100);
let result = cache.get_deltas_since_for_version(2, 100);
let input =
get_deltas_since_input_to_string(cache.session_id_for_version(1), cache.serial(), 100);
get_deltas_since_input_to_string(cache.session_id_for_version(1), cache.serial_for_version(2), 100);
let output = serial_result_detail_to_string(&result);
test_report(
@@ -488,16 +557,16 @@ fn get_deltas_since_returns_reset_required_when_client_serial_is_too_old() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serial(102)
.serials(serials_all(102))
.timing(Timing::default())
.deltas(deltas.clone())
.deltas_by_version(deltas_all(deltas.clone()))
.build();
let result = cache.get_deltas_since(99);
let result = cache.get_deltas_since_for_version(2, 99);
let input = format!(
"{}delta_window:\n{}",
get_deltas_since_input_to_string(cache.session_id_for_version(1), cache.serial(), 99),
get_deltas_since_input_to_string(cache.session_id_for_version(1), cache.serial_for_version(2), 99),
indent_block(&deltas_window_to_string(&deltas), 2),
);
let output = serial_result_detail_to_string(&result);
@@ -552,17 +621,17 @@ fn get_deltas_since_returns_minimal_merged_delta() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serial(103)
.serials(serials_all(103))
.timing(Timing::default())
.snapshot(final_snapshot)
.deltas(deltas.clone())
.snapshots(snapshots_all(final_snapshot))
.deltas_by_version(deltas_all(deltas.clone()))
.build();
let result = cache.get_deltas_since(101);
let result = cache.get_deltas_since_for_version(2, 101);
let input = format!(
"{}delta_window:\n{}",
get_deltas_since_input_to_string(cache.session_id_for_version(1), cache.serial(), 101),
get_deltas_since_input_to_string(cache.session_id_for_version(1), cache.serial_for_version(2), 101),
indent_block(&deltas_window_to_string(&deltas), 2),
);
let output = serial_result_detail_to_string(&result);
@@ -602,14 +671,14 @@ fn get_deltas_since_returns_minimal_merged_delta() {
fn get_deltas_since_returns_reset_required_when_client_serial_is_in_future() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serial(100)
.serials(serials_all(100))
.timing(Timing::default())
.build();
let result = cache.get_deltas_since(101);
let result = cache.get_deltas_since_for_version(2, 101);
let input =
get_deltas_since_input_to_string(cache.session_id_for_version(1), cache.serial(), 101);
get_deltas_since_input_to_string(cache.session_id_for_version(1), cache.serial_for_version(2), 101);
let output = serial_result_detail_to_string(&result);
test_report(
@@ -648,19 +717,19 @@ fn get_deltas_since_supports_incremental_updates_across_serial_wraparound() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serial(0)
.serials(serials_all(0))
.timing(Timing::default())
.snapshot(final_snapshot)
.deltas(deltas.clone())
.snapshots(snapshots_all(final_snapshot))
.deltas_by_version(deltas_all(deltas.clone()))
.build();
let result = cache.get_deltas_since(u32::MAX.wrapping_sub(1));
let result = cache.get_deltas_since_for_version(2, u32::MAX.wrapping_sub(1));
let input = format!(
"{}delta_window:\n{}",
get_deltas_since_input_to_string(
cache.session_id_for_version(1),
cache.serial(),
cache.serial_for_version(2),
u32::MAX.wrapping_sub(1)
),
indent_block(&deltas_window_to_string(&deltas), 2),
@@ -723,24 +792,24 @@ fn get_deltas_since_returns_reset_required_when_client_serial_is_too_old_across_
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serial(1)
.serials(serials_all(1))
.timing(Timing::default())
.snapshot(Snapshot::from_payloads(vec![
.snapshots(snapshots_all(Snapshot::from_payloads(vec![
Payload::RouteOrigin(v4_origin(192, 0, 2, 0, 24, 24, 64496)),
Payload::RouteOrigin(v4_origin(198, 51, 100, 0, 24, 24, 64497)),
Payload::RouteOrigin(v4_origin(203, 0, 113, 0, 24, 24, 64498)),
]))
.deltas(deltas.clone())
])))
.deltas_by_version(deltas_all(deltas.clone()))
.build();
let client_serial = u32::MAX.wrapping_sub(2);
let result = cache.get_deltas_since(client_serial);
let result = cache.get_deltas_since_for_version(2, client_serial);
let input = format!(
"{}delta_window:\n{}",
get_deltas_since_input_to_string(
cache.session_id_for_version(1),
cache.serial(),
cache.serial_for_version(2),
client_serial
),
indent_block(&deltas_window_to_string(&deltas), 2),
@@ -764,14 +833,14 @@ fn get_deltas_since_returns_reset_required_when_client_serial_is_too_old_across_
fn get_deltas_since_returns_reset_required_when_client_serial_is_in_future_across_wraparound() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serial(u32::MAX)
.serials(serials_all(u32::MAX))
.timing(Timing::default())
.build();
let result = cache.get_deltas_since(0);
let result = cache.get_deltas_since_for_version(2, 0);
let input =
get_deltas_since_input_to_string(cache.session_id_for_version(1), cache.serial(), 0);
get_deltas_since_input_to_string(cache.session_id_for_version(1), cache.serial_for_version(2), 0);
let output = serial_result_detail_to_string(&result);
test_report(
@@ -802,9 +871,9 @@ async fn update_no_change_keeps_serial_and_produces_no_delta() {
let mut cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serial(100)
.serials(serials_all(100))
.timing(Timing::default())
.snapshot(snapshot.clone())
.snapshots(snapshots_all(snapshot.clone()))
.build();
let dir = tempfile::tempdir().unwrap();
@@ -814,8 +883,8 @@ async fn update_no_change_keeps_serial_and_produces_no_delta() {
cache.update(new_payloads.clone(), &store).unwrap();
let current_snapshot = cache.snapshot();
let result = cache.get_deltas_since(100);
let current_snapshot = cache.snapshot_for_version(2);
let result = cache.get_deltas_since_for_version(2, 100);
let input = format!(
"old_snapshot :\n{}new_payloads :\n{}",
@@ -825,7 +894,7 @@ async fn update_no_change_keeps_serial_and_produces_no_delta() {
let output = format!(
"cache.serial_after_update: {}\ncurrent_snapshot:\n{}get_deltas_since(100):\n{}",
cache.serial(),
cache.serial_for_version(2),
indent_block(
&snapshot_hashes_and_sorted_view_to_string(&current_snapshot),
2
@@ -840,8 +909,8 @@ async fn update_no_change_keeps_serial_and_produces_no_delta() {
&output,
);
assert_eq!(cache.serial(), 100);
assert!(cache.snapshot().same_content(&snapshot));
assert_eq!(cache.serial_for_version(2), 100);
assert!(cache.snapshot_for_version(2).same_content(&snapshot));
match result {
SerialResult::UpToDate => {}
@@ -861,9 +930,9 @@ async fn update_add_only_increments_serial_and_generates_announced_delta() {
let mut cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serial(100)
.serials(serials_all(100))
.timing(Timing::default())
.snapshot(old_snapshot.clone())
.snapshots(snapshots_all(old_snapshot.clone()))
.build();
let dir = tempfile::tempdir().unwrap();
@@ -876,8 +945,8 @@ async fn update_add_only_increments_serial_and_generates_announced_delta() {
cache.update(new_payloads.clone(), &store).unwrap();
let current_snapshot = cache.snapshot();
let result = cache.get_deltas_since(100);
let current_snapshot = cache.snapshot_for_version(2);
let result = cache.get_deltas_since_for_version(2, 100);
let input = format!(
"old_snapshot :\n{}new_payloads :\n{}",
@@ -887,7 +956,7 @@ async fn update_add_only_increments_serial_and_generates_announced_delta() {
let output = format!(
"cache.serial_after_update: {}\ncurrent_snapshot:\n{}get_deltas_since(100):\n{}",
cache.serial(),
cache.serial_for_version(2),
indent_block(
&snapshot_hashes_and_sorted_view_to_string(&current_snapshot),
2
@@ -902,7 +971,7 @@ async fn update_add_only_increments_serial_and_generates_announced_delta() {
&output,
);
assert_eq!(cache.serial(), 101);
assert_eq!(cache.serial_for_version(2), 101);
match result {
SerialResult::Delta(delta) => {
@@ -934,9 +1003,9 @@ async fn update_remove_only_increments_serial_and_generates_withdrawn_delta() {
let mut cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serial(100)
.serials(serials_all(100))
.timing(Timing::default())
.snapshot(old_snapshot.clone())
.snapshots(snapshots_all(old_snapshot.clone()))
.build();
let dir = tempfile::tempdir().unwrap();
@@ -946,8 +1015,8 @@ async fn update_remove_only_increments_serial_and_generates_withdrawn_delta() {
cache.update(new_payloads.clone(), &store).unwrap();
let current_snapshot = cache.snapshot();
let result = cache.get_deltas_since(100);
let current_snapshot = cache.snapshot_for_version(2);
let result = cache.get_deltas_since_for_version(2, 100);
let input = format!(
"old_snapshot :\n{}new_payloads :\n{}",
@@ -957,7 +1026,7 @@ async fn update_remove_only_increments_serial_and_generates_withdrawn_delta() {
let output = format!(
"cache.serial_after_update: {}\ncurrent_snapshot:\n{}get_deltas_since(100):\n{}",
cache.serial(),
cache.serial_for_version(2),
indent_block(
&snapshot_hashes_and_sorted_view_to_string(&current_snapshot),
2
@@ -972,7 +1041,7 @@ async fn update_remove_only_increments_serial_and_generates_withdrawn_delta() {
&output,
);
assert_eq!(cache.serial(), 101);
assert_eq!(cache.serial_for_version(2), 101);
match result {
SerialResult::Delta(delta) => {
@@ -1010,9 +1079,9 @@ async fn update_add_and_remove_increments_serial_and_generates_both_sides() {
let mut cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serial(100)
.serials(serials_all(100))
.timing(Timing::default())
.snapshot(old_snapshot.clone())
.snapshots(snapshots_all(old_snapshot.clone()))
.build();
let dir = tempfile::tempdir().unwrap();
@@ -1025,8 +1094,8 @@ async fn update_add_and_remove_increments_serial_and_generates_both_sides() {
cache.update(new_payloads.clone(), &store).unwrap();
let current_snapshot = cache.snapshot();
let result = cache.get_deltas_since(100);
let current_snapshot = cache.snapshot_for_version(2);
let result = cache.get_deltas_since_for_version(2, 100);
let input = format!(
"old_snapshot :\n{}new_payloads :\n{}",
@@ -1036,7 +1105,7 @@ async fn update_add_and_remove_increments_serial_and_generates_both_sides() {
let output = format!(
"cache.serial_after_update: {}\ncurrent_snapshot:\n{}get_deltas_since(100):\n{}",
cache.serial(),
cache.serial_for_version(2),
indent_block(
&snapshot_hashes_and_sorted_view_to_string(&current_snapshot),
2
@@ -1051,7 +1120,7 @@ async fn update_add_and_remove_increments_serial_and_generates_both_sides() {
&output,
);
assert_eq!(cache.serial(), 101);
assert_eq!(cache.serial_for_version(2), 101);
match result {
SerialResult::Delta(delta) => {
@@ -1099,17 +1168,17 @@ fn get_deltas_since_cancels_announce_then_withdraw_for_same_prefix() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serial(102)
.serials(serials_all(102))
.timing(Timing::default())
.snapshot(final_snapshot)
.deltas(deltas.clone())
.snapshots(snapshots_all(final_snapshot))
.deltas_by_version(deltas_all(deltas.clone()))
.build();
let result = cache.get_deltas_since(100);
let result = cache.get_deltas_since_for_version(2, 100);
let input = format!(
"{}delta_window:\n{}",
get_deltas_since_input_to_string(cache.session_id_for_version(1), cache.serial(), 100),
get_deltas_since_input_to_string(cache.session_id_for_version(1), cache.serial_for_version(2), 100),
indent_block(&deltas_window_to_string(&deltas), 2),
);
let output = serial_result_detail_to_string(&result);
@@ -1153,17 +1222,17 @@ fn get_deltas_since_cancels_withdraw_then_announce_for_same_prefix() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serial(102)
.serials(serials_all(102))
.timing(Timing::default())
.snapshot(final_snapshot)
.deltas(deltas.clone())
.snapshots(snapshots_all(final_snapshot))
.deltas_by_version(deltas_all(deltas.clone()))
.build();
let result = cache.get_deltas_since(100);
let result = cache.get_deltas_since_for_version(2, 100);
let input = format!(
"{}delta_window:\n{}",
get_deltas_since_input_to_string(cache.session_id_for_version(1), cache.serial(), 100),
get_deltas_since_input_to_string(cache.session_id_for_version(1), cache.serial_for_version(2), 100),
indent_block(&deltas_window_to_string(&deltas), 2),
);
let output = serial_result_detail_to_string(&result);
@@ -1207,17 +1276,17 @@ fn get_deltas_since_merges_replacement_into_withdraw_and_announce() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serial(102)
.serials(serials_all(102))
.timing(Timing::default())
.snapshot(final_snapshot)
.deltas(deltas.clone())
.snapshots(snapshots_all(final_snapshot))
.deltas_by_version(deltas_all(deltas.clone()))
.build();
let result = cache.get_deltas_since(100);
let result = cache.get_deltas_since_for_version(2, 100);
let input = format!(
"{}delta_window:\n{}",
get_deltas_since_input_to_string(cache.session_id_for_version(1), cache.serial(), 100),
get_deltas_since_input_to_string(cache.session_id_for_version(1), cache.serial_for_version(2), 100),
indent_block(&deltas_window_to_string(&deltas), 2),
);
let output = serial_result_detail_to_string(&result);
@@ -1285,18 +1354,18 @@ fn get_deltas_since_merges_multiple_deltas_to_final_minimal_view() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serial(103)
.serials(serials_all(103))
.timing(Timing::default())
.snapshot(final_snapshot)
.deltas(deltas.clone())
.snapshots(snapshots_all(final_snapshot))
.deltas_by_version(deltas_all(deltas.clone()))
.build();
// ?serial=100 A/B ?+C
let result = cache.get_deltas_since(100);
let result = cache.get_deltas_since_for_version(2, 100);
let input = format!(
"{}delta_window:\n{}",
get_deltas_since_input_to_string(cache.session_id_for_version(1), cache.serial(), 100),
get_deltas_since_input_to_string(cache.session_id_for_version(1), cache.serial_for_version(2), 100),
indent_block(&deltas_window_to_string(&deltas), 2),
);
let output = serial_result_detail_to_string(&result);
@@ -1381,13 +1450,13 @@ fn get_deltas_since_merges_aspa_replacement_into_single_announcement() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serial(102)
.serials(serials_all(102))
.timing(Timing::default())
.snapshot(Snapshot::from_payloads(vec![Payload::Aspa(new.clone())]))
.deltas(deltas)
.snapshots(snapshots_all(Snapshot::from_payloads(vec![Payload::Aspa(new.clone())])))
.deltas_by_version(deltas_all(deltas))
.build();
let result = cache.get_deltas_since(100);
let result = cache.get_deltas_since_for_version(2, 100);
match result {
SerialResult::Delta(delta) => {

View File

@@ -13,7 +13,10 @@ fn fixture_path(name: &str) -> PathBuf {
#[test]
#[ignore = "manual CCR loader smoke test for local samples"]
fn ccr_loader_smoke() {
let snapshot = load_ccr_snapshot_from_file(fixture_path("20260324T000037Z-sng1.ccr"))
// let path = "./mini_data/20260403T000001Z-mini-a.ccr";
// let path = "20260403T000101Z-mini-b.ccr";
let path = "20260403T000201Z-mini-c.ccr";
let snapshot = load_ccr_snapshot_from_file(fixture_path(path))
.expect("load CCR snapshot");
println!("content_type_oid: {}", snapshot.content_type_oid);
@@ -22,6 +25,8 @@ fn ccr_loader_smoke() {
println!("vap_count : {}", snapshot.vaps.len());
println!("first_vrp : {:?}", snapshot.vrps.first());
println!("first_vap : {:?}", snapshot.vaps.first());
println!("vrps : {:?}", snapshot.vrps);
println!("vaps : {:?}", snapshot.vaps);
}
#[test]
@@ -65,3 +70,19 @@ fn snapshot_to_payloads_with_options_skips_invalid_aspa_when_not_strict() {
assert_eq!(conversion.invalid_vaps.len(), 1);
assert!(conversion.invalid_vaps[0].contains("provider list must not contain AS0"));
}
#[test]
fn generated_mini_ccr_files_are_parseable() {
let cases = [
("20260403T000001Z-mini-a.ccr", 2usize, 1usize),
("20260403T000101Z-mini-b.ccr", 3usize, 1usize),
("20260403T000201Z-mini-c.ccr", 2usize, 2usize),
];
for (name, expect_vrps, expect_vaps) in cases {
let snapshot = load_ccr_snapshot_from_file(fixture_path(name))
.unwrap_or_else(|e| panic!("failed to parse {}: {:?}", name, e));
assert_eq!(snapshot.vrps.len(), expect_vrps, "vrp count mismatch for {name}");
assert_eq!(snapshot.vaps.len(), expect_vaps, "vap count mismatch for {name}");
}
}

View File

@@ -1,4 +1,4 @@
mod common;
mod common;
use std::collections::VecDeque;
use std::fs::File;
@@ -31,6 +31,7 @@ use rpki::rtr::pdu::{
Aspa as AspaPdu, CacheReset, CacheResponse, EndOfDataV1, ErrorReport, Header, IPv4Prefix,
IPv6Prefix, ResetQuery, RouterKey as RouterKeyPdu, SerialNotify, SerialQuery,
};
use rpki::rtr::store::RtrStore;
use rpki::rtr::server::connection::handle_tls_connection;
use rpki::rtr::server::tls::load_rustls_server_config_with_options;
use rpki::rtr::session::RtrSession;
@@ -273,6 +274,42 @@ fn assert_error_report_matches(
assert_eq!(report.erroneous_pdu(), offending_pdu);
}
fn serials_all(serial: u32) -> [u32; 3] {
[serial; 3]
}
fn snapshots_all(snapshot: Snapshot) -> [Snapshot; 3] {
[snapshot.clone(), snapshot.clone(), snapshot]
}
fn deltas_all(deltas: VecDeque<Arc<Delta>>) -> [VecDeque<Arc<Delta>>; 3] {
[deltas.clone(), deltas.clone(), deltas]
}
async fn wait_for_store_serials(store: &RtrStore, expected: [u32; 3]) {
for _ in 0..100 {
let current = [
store.get_serial_for_version(0).unwrap(),
store.get_serial_for_version(1).unwrap(),
store.get_serial_for_version(2).unwrap(),
];
if current == [Some(expected[0]), Some(expected[1]), Some(expected[2])] {
return;
}
tokio::time::sleep(Duration::from_millis(20)).await;
}
panic!(
"timed out waiting store serials {:?}, current={:?}",
expected,
[
store.get_serial_for_version(0).unwrap(),
store.get_serial_for_version(1).unwrap(),
store.get_serial_for_version(2).unwrap(),
]
);
}
/// 测试:Reset Query 会返回完整 snapshot,并以 End of Data 结束响应。
#[tokio::test]
async fn reset_query_returns_snapshot_and_end_of_data() {
@@ -285,9 +322,9 @@ async fn reset_query_returns_snapshot_and_end_of_data() {
let snapshot = Snapshot::from_payloads(vec![Payload::RouteOrigin(origin)]);
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serial(100)
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.snapshot(snapshot)
.snapshots(snapshots_all(snapshot))
.build();
let server_cache = shared_cache(cache);
@@ -333,7 +370,7 @@ async fn reset_query_returns_snapshot_and_end_of_data() {
async fn reset_query_uses_version_specific_session_id() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([40, 41, 42]))
.serial(100)
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.build();
@@ -360,12 +397,98 @@ async fn reset_query_uses_version_specific_session_id() {
shutdown_server(client, shutdown_tx, server_handle).await;
}
#[tokio::test]
async fn restart_restores_versioned_state_and_serves_queries() {
let dir = tempfile::tempdir().unwrap();
let store = RtrStore::open(dir.path()).unwrap();
let prefix = IPAddressPrefix {
address: IPAddress::from_ipv4(Ipv4Addr::new(192, 0, 2, 0)),
prefix_length: 24,
};
let origin = Payload::RouteOrigin(RouteOrigin::new(prefix, 24, 64496u32.into()));
let valid_spki = vec![
0x30, 0x13, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01,
0x01, 0x05, 0x00, 0x03, 0x02, 0x00, 0x00,
];
let router_key = Payload::RouterKey(RouterKey::new(
Ski::default(),
Asn::from(64496u32),
valid_spki.clone(),
));
let aspa_old = Payload::Aspa(Aspa::new(Asn::from(64496u32), vec![Asn::from(64497u32)]));
let aspa_new = Payload::Aspa(Aspa::new(
Asn::from(64496u32),
vec![Asn::from(64497u32), Asn::from(64498u32)],
));
let initial_payloads = vec![origin.clone(), router_key.clone(), aspa_old];
let mut cache = rpki::rtr::cache::RtrCache::default()
.init(&store, 16, false, Timing::new(600, 600, 7200), || {
Ok(initial_payloads.clone())
})
.unwrap();
cache
.update(vec![origin.clone(), router_key.clone(), aspa_new], &store)
.unwrap();
wait_for_store_serials(&store, [1, 1, 2]).await;
let restored = rpki::rtr::cache::RtrCache::default()
.init(&store, 16, false, Timing::new(600, 600, 7200), || {
panic!("file loader should not be used when restoring from store")
})
.unwrap();
assert_eq!(restored.serial_for_version(0), 1);
assert_eq!(restored.serial_for_version(1), 1);
assert_eq!(restored.serial_for_version(2), 2);
let shared = shared_cache(restored);
let (addr, _notify_tx, shutdown_tx, server_handle) = start_session_server(shared.clone()).await;
let mut client = TcpStream::connect(addr).await.unwrap();
ResetQuery::new(0).write(&mut client).await.unwrap();
let response = CacheResponse::read(&mut client).await.unwrap();
assert_eq!(response.version(), 0);
let expected_sid_v0 = shared.read().unwrap().session_id_for_version(0);
assert_eq!(response.session_id(), expected_sid_v0);
let _v4 = IPv4Prefix::read(&mut client).await.unwrap();
let eod_v0 = rpki::rtr::pdu::EndOfDataV0::read(&mut client).await.unwrap();
assert_eq!(eod_v0.serial_number(), 1);
shutdown_server(client, shutdown_tx, server_handle).await;
let (addr, _notify_tx, shutdown_tx, server_handle) = start_session_server(shared.clone()).await;
let mut client = TcpStream::connect(addr).await.unwrap();
ResetQuery::new(1).write(&mut client).await.unwrap();
let response = CacheResponse::read(&mut client).await.unwrap();
assert_eq!(response.version(), 1);
let expected_sid_v1 = shared.read().unwrap().session_id_for_version(1);
assert_eq!(response.session_id(), expected_sid_v1);
let _v4 = IPv4Prefix::read(&mut client).await.unwrap();
let _rk = RouterKeyPdu::read(&mut client).await.unwrap();
let eod_v1 = EndOfDataV1::read(&mut client).await.unwrap();
assert_eq!(eod_v1.serial_number(), 1);
shutdown_server(client, shutdown_tx, server_handle).await;
let (addr, _notify_tx, shutdown_tx, server_handle) = start_session_server(shared.clone()).await;
let mut client = TcpStream::connect(addr).await.unwrap();
let sid_v2 = shared.read().unwrap().session_id_for_version(2);
SerialQuery::new(2, sid_v2, 1).write(&mut client).await.unwrap();
let response = CacheResponse::read(&mut client).await.unwrap();
assert_eq!(response.version(), 2);
assert_eq!(response.session_id(), sid_v2);
let _aspa = AspaPdu::read(&mut client).await.unwrap();
let eod_v2 = EndOfDataV1::read(&mut client).await.unwrap();
assert_eq!(eod_v2.serial_number(), 2);
shutdown_server(client, shutdown_tx, server_handle).await;
}
/// 测试:当 Serial Query 的 session_id 和 serial 都与当前 cache 一致时,仅返回 End of Data。
#[tokio::test]
async fn serial_query_returns_end_of_data_when_up_to_date() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serial(100)
.serials(serials_all(100))
.timing(Timing {
refresh: 600,
retry: 600,
@@ -402,7 +525,7 @@ async fn serial_query_returns_end_of_data_when_up_to_date() {
async fn serial_query_returns_corrupt_data_when_session_id_mismatch() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serial(100)
.serials(serials_all(100))
.timing(Timing {
refresh: 600,
retry: 600,
@@ -457,13 +580,13 @@ async fn serial_query_returns_deltas_when_incremental_update_available() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serial(101)
.serials(serials_all(101))
.timing(Timing {
refresh: 600,
retry: 600,
expire: 7200,
})
.deltas(deltas)
.deltas_by_version(deltas_all(deltas))
.build();
let server_cache = shared_cache(cache);
@@ -542,10 +665,10 @@ async fn serial_query_returns_deltas_across_serial_wraparound() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serial(0)
.serials(serials_all(0))
.timing(Timing::new(600, 600, 7200))
.snapshot(snapshot)
.deltas(deltas)
.snapshots(snapshots_all(snapshot))
.deltas_by_version(deltas_all(deltas))
.build();
let server_cache = shared_cache(cache);
@@ -587,7 +710,7 @@ async fn serial_query_returns_deltas_across_serial_wraparound() {
async fn serial_query_returns_cache_reset_for_future_serial_across_wraparound() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serial(u32::MAX)
.serials(serials_all(u32::MAX))
.timing(Timing::new(600, 600, 7200))
.build();
@@ -637,9 +760,9 @@ async fn reset_query_returns_payloads_in_rtr_order() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serial(100)
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.snapshot(snapshot)
.snapshots(snapshots_all(snapshot))
.build();
let server_cache = shared_cache(cache);
@@ -748,13 +871,13 @@ async fn serial_query_returns_announcements_before_withdrawals() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serial(101)
.serials(serials_all(101))
.timing(Timing {
refresh: 600,
retry: 600,
expire: 7200,
})
.deltas(deltas)
.deltas_by_version(deltas_all(deltas))
.build();
let server_cache = shared_cache(cache);
@@ -840,7 +963,7 @@ async fn serial_query_returns_announcements_before_withdrawals() {
async fn established_session_sends_serial_notify() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serial(100)
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.build();
@@ -882,7 +1005,7 @@ async fn established_session_sends_serial_notify() {
async fn first_pdu_with_too_high_version_returns_unsupported_version_error() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serial(100)
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.build();
@@ -922,7 +1045,7 @@ async fn first_pdu_with_too_high_version_returns_unsupported_version_error() {
async fn session_rejects_version_change_after_negotiation() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serial(100)
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.build();
@@ -975,7 +1098,7 @@ async fn session_rejects_version_change_after_negotiation() {
async fn notify_is_not_sent_before_version_negotiation() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serial(100)
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.build();
@@ -1012,7 +1135,7 @@ async fn notify_is_not_sent_before_version_negotiation() {
async fn serial_notify_is_rate_limited_to_once_per_minute() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serial(100)
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.build();
@@ -1073,7 +1196,7 @@ async fn reset_query_returns_no_data_available_when_cache_is_unavailable() {
let cache = RtrCacheBuilder::new()
.availability(rpki::rtr::cache::CacheAvailability::NoDataAvailable)
.session_ids(SessionIds::from_array([42, 42, 42]))
.serial(100)
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.build();
@@ -1108,7 +1231,7 @@ async fn serial_query_returns_no_data_available_when_cache_is_unavailable() {
let cache = RtrCacheBuilder::new()
.availability(rpki::rtr::cache::CacheAvailability::NoDataAvailable)
.session_ids(SessionIds::from_array([42, 42, 42]))
.serial(100)
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.build();
@@ -1142,7 +1265,7 @@ async fn serial_query_returns_no_data_available_when_cache_is_unavailable() {
async fn first_pdu_with_invalid_length_returns_corrupt_data() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serial(100)
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.build();
@@ -1211,7 +1334,7 @@ async fn first_pdu_with_invalid_length_returns_corrupt_data() {
async fn established_session_closes_after_receiving_error_report() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serial(100)
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.build();
@@ -1260,7 +1383,7 @@ async fn established_session_closes_after_receiving_error_report() {
async fn established_session_invalid_header_returns_corrupt_data() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serial(100)
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.build();
@@ -1324,7 +1447,7 @@ async fn established_session_invalid_header_returns_corrupt_data() {
async fn established_session_unknown_pdu_returns_unsupported_pdu_type() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serial(100)
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.build();
@@ -1393,9 +1516,9 @@ async fn version_zero_does_not_send_router_key_or_aspa() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serial(100)
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.snapshot(snapshot)
.snapshots(snapshots_all(snapshot))
.build();
let server_cache = shared_cache(cache);
@@ -1436,9 +1559,9 @@ async fn version_two_aspa_withdraw_has_empty_provider_list() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serial(101)
.serials(serials_all(101))
.timing(Timing::new(600, 600, 7200))
.deltas(deltas)
.deltas_by_version(deltas_all(deltas))
.build();
let server_cache = shared_cache(cache);
@@ -1477,7 +1600,11 @@ async fn version_two_aspa_withdraw_has_empty_provider_list() {
#[tokio::test]
async fn version_one_sends_router_key_but_not_aspa() {
let router_key = RouterKey::new(Ski::default(), Asn::from(64496u32), vec![1u8; 32]);
let valid_spki = vec![
0x30, 0x13, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01,
0x01, 0x05, 0x00, 0x03, 0x02, 0x00, 0x00,
];
let router_key = RouterKey::new(Ski::default(), Asn::from(64496u32), valid_spki);
let aspa = Aspa::new(Asn::from(64496u32), vec![Asn::from(64497u32)]);
let snapshot = Snapshot::from_payloads(vec![
Payload::RouterKey(router_key),
@@ -1486,9 +1613,9 @@ async fn version_one_sends_router_key_but_not_aspa() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serial(100)
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.snapshot(snapshot)
.snapshots(snapshots_all(snapshot))
.build();
let server_cache = shared_cache(cache);
@@ -1530,7 +1657,7 @@ async fn version_one_sends_router_key_but_not_aspa() {
async fn established_session_idle_timeout_returns_transport_failed() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serial(100)
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.build();
@@ -1588,7 +1715,7 @@ async fn established_session_idle_timeout_returns_transport_failed() {
async fn tls_client_with_matching_san_ip_is_accepted() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serial(100)
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.build();
@@ -1618,7 +1745,7 @@ async fn tls_client_with_matching_san_ip_is_accepted() {
async fn tls_client_accepts_server_certificate_with_dns_san() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serial(100)
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.build();
@@ -1655,7 +1782,7 @@ async fn tls_client_accepts_server_certificate_with_dns_san() {
async fn tls_server_dns_name_san_strict_mode_rejects_ip_only_certificate() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serial(100)
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.build();
@@ -1678,7 +1805,7 @@ async fn tls_server_dns_name_san_strict_mode_rejects_ip_only_certificate() {
async fn tls_client_with_mismatched_san_ip_is_rejected() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serial(100)
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.build();
@@ -1716,7 +1843,7 @@ async fn tls_client_with_mismatched_san_ip_is_rejected() {
async fn invalid_timing_prevents_end_of_data_response() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serial(100)
.serials(serials_all(100))
.timing(Timing::new(600, 8000, 7200))
.build();
@@ -1752,9 +1879,9 @@ async fn invalid_aspa_prevents_snapshot_response() {
))]);
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serial(100)
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.snapshot(snapshot)
.snapshots(snapshots_all(snapshot))
.build();
let server_cache = shared_cache(cache);
@@ -1790,9 +1917,9 @@ async fn invalid_router_key_prevents_snapshot_response() {
))]);
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serial(100)
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.snapshot(snapshot)
.snapshots(snapshots_all(snapshot))
.build();
let server_cache = shared_cache(cache);

View File

@@ -0,0 +1,45 @@
use std::fs;
use std::path::{Path, PathBuf};
fn collect_rs_files(dir: &Path, out: &mut Vec<PathBuf>) {
let entries = fs::read_dir(dir).unwrap();
for entry in entries {
let entry = entry.unwrap();
let path = entry.path();
if path.is_dir() {
collect_rs_files(&path, out);
} else if path.extension().and_then(|s| s.to_str()) == Some("rs") {
out.push(path);
}
}
}
#[test]
fn save_cache_state_versioned_has_limited_call_sites() {
let repo_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let mut files = Vec::new();
collect_rs_files(&repo_root.join("src"), &mut files);
collect_rs_files(&repo_root.join("tests"), &mut files);
let allowed = [
repo_root.join("src/rtr/store.rs"),
repo_root.join("src/rtr/cache/store.rs"),
repo_root.join("tests/test_store_db.rs"),
repo_root.join("tests/test_cache.rs"),
repo_root.join("tests/test_store_boundary.rs"),
];
let mut violations = Vec::new();
for file in files {
let content = fs::read_to_string(&file).unwrap();
if content.contains(".save_cache_state_versioned(") && !allowed.contains(&file) {
violations.push(file);
}
}
assert!(
violations.is_empty(),
"unexpected direct save_cache_state_versioned() call sites: {:?}",
violations
);
}

View File

@@ -2,371 +2,110 @@ mod common;
use std::net::Ipv6Addr;
use common::test_helper::{
indent_block, payloads_to_string, test_report, v4_origin, v6_origin,
};
use common::test_helper::{v4_origin, v6_origin};
use rpki::rtr::cache::{CacheAvailability, Delta, SessionIds, Snapshot};
use rpki::rtr::cache::{CacheAvailability, Delta, Snapshot};
use rpki::rtr::payload::Payload;
use rpki::rtr::store::RtrStore;
fn snapshot_to_string(snapshot: &Snapshot) -> String {
let payloads = snapshot.payloads_for_rtr();
payloads_to_string(&payloads)
}
fn delta_to_string(delta: &Delta) -> String {
format!(
"serial: {}\nannounced:\n{}withdrawn:\n{}",
delta.serial(),
indent_block(&payloads_to_string(delta.announced()), 2),
indent_block(&payloads_to_string(delta.withdrawn()), 2),
)
}
#[test]
fn store_db_save_and_get_snapshot() {
fn store_db_versioned_state_persists_and_restores_all_versions() {
let dir = tempfile::tempdir().unwrap();
let store = RtrStore::open(dir.path()).unwrap();
let input_payloads = vec![
Payload::RouteOrigin(v4_origin(192, 0, 2, 0, 24, 24, 64496)),
Payload::RouteOrigin(v6_origin(
let snapshots = [
Snapshot::from_payloads(vec![Payload::RouteOrigin(v4_origin(
192, 0, 2, 0, 24, 24, 64496,
))]),
Snapshot::from_payloads(vec![
Payload::RouteOrigin(v4_origin(192, 0, 2, 0, 24, 24, 64496)),
Payload::RouteOrigin(v4_origin(198, 51, 100, 0, 24, 24, 64497)),
]),
Snapshot::from_payloads(vec![
Payload::RouteOrigin(v4_origin(192, 0, 2, 0, 24, 24, 64496)),
Payload::RouteOrigin(v4_origin(198, 51, 100, 0, 24, 24, 64497)),
Payload::RouteOrigin(v6_origin(
Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0),
32,
48,
64498,
)),
]),
];
let session_ids = [410u16, 411u16, 412u16];
let serials = [100u32, 200u32, 300u32];
let d0 = Delta::new(
100,
vec![Payload::RouteOrigin(v4_origin(192, 0, 2, 0, 24, 24, 64496))],
vec![],
);
let d2 = Delta::new(
300,
vec![Payload::RouteOrigin(v6_origin(
Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0),
32,
48,
64497,
)),
];
let snapshot = Snapshot::from_payloads(input_payloads.clone());
store.save_snapshot(&snapshot).unwrap();
let loaded = store.get_snapshot().unwrap().expect("snapshot should exist");
let input = format!(
"db_path: {}\nsnapshot:\n{}",
dir.path().display(),
indent_block(&payloads_to_string(&input_payloads), 2),
);
let output = format!(
"loaded snapshot:\n{}same_content: {}\n",
indent_block(&snapshot_to_string(&loaded), 2),
snapshot.same_content(&loaded),
);
test_report(
"store_db_save_and_get_snapshot",
"验证 save_snapshot() 后可以通过 get_snapshot() 正确读回 Snapshot。",
&input,
&output,
);
assert!(snapshot.same_content(&loaded));
}
#[test]
fn store_db_set_and_get_meta_fields() {
let dir = tempfile::tempdir().unwrap();
let store = RtrStore::open(dir.path()).unwrap();
let session_ids = SessionIds::from_array([40, 41, 42]);
store.set_session_ids(&session_ids).unwrap();
store.set_serial(100).unwrap();
store.set_delta_window(101, 110).unwrap();
let loaded_session_ids = store.get_session_ids().unwrap();
let serial = store.get_serial().unwrap();
let window = store.get_delta_window().unwrap();
let input = format!(
"db_path: {}\nset_session_ids={:?}\nset_serial=100\nset_delta_window=(101, 110)\n",
dir.path().display(),
session_ids,
);
let output = format!(
"get_session_ids: {:?}\nget_serial: {:?}\nget_delta_window: {:?}\n",
loaded_session_ids, serial, window,
);
test_report(
"store_db_set_and_get_meta_fields",
"验证 session_ids / serial / delta_window 能正确写入并读回。",
&input,
&output,
);
assert_eq!(loaded_session_ids, Some(session_ids));
assert_eq!(serial, Some(100));
assert_eq!(window, Some((101, 110)));
}
#[test]
fn store_db_clear_delta_window_removes_both_bounds() {
let dir = tempfile::tempdir().unwrap();
let store = RtrStore::open(dir.path()).unwrap();
store.set_delta_window(101, 110).unwrap();
assert_eq!(store.get_delta_window().unwrap(), Some((101, 110)));
store.clear_delta_window().unwrap();
assert_eq!(store.get_delta_window().unwrap(), None);
}
#[test]
fn store_db_save_and_get_delta() {
let dir = tempfile::tempdir().unwrap();
let store = RtrStore::open(dir.path()).unwrap();
let delta = Delta::new(
101,
vec![Payload::RouteOrigin(v4_origin(198, 51, 100, 0, 24, 24, 64497))],
vec![Payload::RouteOrigin(v4_origin(192, 0, 2, 0, 24, 24, 64496))],
);
store.save_delta(&delta).unwrap();
let loaded = store.get_delta(101).unwrap().expect("delta should exist");
let input = format!(
"db_path: {}\ndelta:\n{}",
dir.path().display(),
indent_block(&delta_to_string(&delta), 2),
);
let output = format!(
"loaded delta:\n{}",
indent_block(&delta_to_string(&loaded), 2),
);
test_report(
"store_db_save_and_get_delta",
"验证 save_delta() 后可以通过 get_delta(serial) 正确读回 Delta。",
&input,
&output,
);
assert_eq!(loaded.serial(), 101);
assert_eq!(loaded.announced().len(), 1);
assert_eq!(loaded.withdrawn().len(), 1);
}
#[test]
fn store_db_load_deltas_since_returns_only_newer_deltas_in_order() {
let dir = tempfile::tempdir().unwrap();
let store = RtrStore::open(dir.path()).unwrap();
let d101 = Delta::new(
101,
vec![Payload::RouteOrigin(v4_origin(192, 0, 2, 0, 24, 24, 64496))],
64498,
))],
vec![],
);
let d102 = Delta::new(
102,
vec![Payload::RouteOrigin(v4_origin(198, 51, 100, 0, 24, 24, 64497))],
vec![],
);
let d103 = Delta::new(
103,
vec![Payload::RouteOrigin(v4_origin(203, 0, 113, 0, 24, 24, 64498))],
vec![],
);
store.save_delta(&d101).unwrap();
store.save_delta(&d102).unwrap();
store.save_delta(&d103).unwrap();
let loaded = store.load_deltas_since(101).unwrap();
let input = format!(
"db_path: {}\nsaved delta serials: [101, 102, 103]\nload_deltas_since(101)\n",
dir.path().display(),
);
let output = {
let mut s = String::new();
for (idx, d) in loaded.iter().enumerate() {
s.push_str(&format!("loaded[{}]:\n", idx));
s.push_str(&indent_block(&delta_to_string(d), 2));
}
s
};
test_report(
"store_db_load_deltas_since_returns_only_newer_deltas_in_order",
"验证 load_deltas_since(x) 只返回 serial > x 的 Delta,且顺序正确。",
&input,
&output,
);
assert_eq!(loaded.len(), 2);
assert_eq!(loaded[0].serial(), 102);
assert_eq!(loaded[1].serial(), 103);
}
#[test]
fn store_db_save_snapshot_and_meta_writes_all_fields() {
let dir = tempfile::tempdir().unwrap();
let store = RtrStore::open(dir.path()).unwrap();
let session_ids = SessionIds::from_array([40, 41, 42]);
let snapshot = Snapshot::from_payloads(vec![
Payload::RouteOrigin(v4_origin(192, 0, 2, 0, 24, 24, 64496)),
Payload::RouteOrigin(v4_origin(198, 51, 100, 0, 24, 24, 64497)),
]);
store
.save_snapshot_and_meta(&snapshot, &session_ids, 100)
.unwrap();
let loaded_snapshot = store.get_snapshot().unwrap().expect("snapshot should exist");
let loaded_session_ids = store.get_session_ids().unwrap();
let loaded_serial = store.get_serial().unwrap();
let input = format!(
"db_path: {}\nsnapshot:\n{}session_ids={:?}\nserial=100\n",
dir.path().display(),
indent_block(&snapshot_to_string(&snapshot), 2),
session_ids,
);
let output = format!(
"loaded_snapshot:\n{}loaded_session_ids: {:?}\nloaded_serial: {:?}\n",
indent_block(&snapshot_to_string(&loaded_snapshot), 2),
loaded_session_ids,
loaded_serial,
);
test_report(
"store_db_save_snapshot_and_meta_writes_all_fields",
"验证 save_snapshot_and_meta() 会同时写入 snapshot、session_ids 和 serial。",
&input,
&output,
);
assert!(snapshot.same_content(&loaded_snapshot));
assert_eq!(loaded_session_ids, Some(session_ids));
assert_eq!(loaded_serial, Some(100));
}
#[test]
fn store_db_save_cache_state_writes_delta_snapshot_meta_and_window_together() {
let dir = tempfile::tempdir().unwrap();
let store = RtrStore::open(dir.path()).unwrap();
let session_ids = SessionIds::from_array([40, 41, 42]);
let snapshot = Snapshot::from_payloads(vec![
Payload::RouteOrigin(v4_origin(192, 0, 2, 0, 24, 24, 64496)),
Payload::RouteOrigin(v4_origin(198, 51, 100, 0, 24, 24, 64497)),
]);
let delta = Delta::new(
101,
vec![Payload::RouteOrigin(v4_origin(198, 51, 100, 0, 24, 24, 64497))],
vec![Payload::RouteOrigin(v4_origin(192, 0, 2, 0, 24, 24, 64496))],
);
store
.save_cache_state(
.save_cache_state_versioned(
CacheAvailability::Ready,
&snapshot,
&snapshots,
&session_ids,
101,
Some(&delta),
Some((101, 101)),
false,
&serials,
&[Some(&d0), None, Some(&d2)],
&[Some((100, 100)), None, Some((300, 300))],
&[false, false, false],
)
.unwrap();
let loaded_snapshot = store.get_snapshot().unwrap().expect("snapshot should exist");
let loaded_session_ids = store.get_session_ids().unwrap();
let loaded_serial = store.get_serial().unwrap();
let loaded_availability = store.get_availability().unwrap();
let loaded_delta = store.get_delta(101).unwrap().expect("delta should exist");
let loaded_window = store.get_delta_window().unwrap();
assert_eq!(store.get_availability().unwrap(), Some(CacheAvailability::Ready));
assert!(snapshot.same_content(&loaded_snapshot));
assert_eq!(loaded_session_ids, Some(session_ids));
assert_eq!(loaded_serial, Some(101));
assert_eq!(loaded_availability, Some(CacheAvailability::Ready));
assert_eq!(loaded_delta.serial(), 101);
assert_eq!(loaded_window, Some((101, 101)));
for version in 0u8..=2 {
let idx = version as usize;
let loaded_snapshot = store
.get_snapshot_for_version(version)
.unwrap()
.expect("snapshot should exist");
let loaded_session_id = store
.get_session_id_for_version(version)
.unwrap()
.expect("session_id should exist");
let loaded_serial = store
.get_serial_for_version(version)
.unwrap()
.expect("serial should exist");
assert!(snapshots[idx].same_content(&loaded_snapshot));
assert_eq!(loaded_session_id, session_ids[idx]);
assert_eq!(loaded_serial, serials[idx]);
}
assert_eq!(store.get_delta_window_for_version(0).unwrap(), Some((100, 100)));
assert_eq!(store.get_delta_window_for_version(1).unwrap(), None);
assert_eq!(store.get_delta_window_for_version(2).unwrap(), Some((300, 300)));
assert_eq!(
store.get_delta_for_version(0, 100).unwrap().map(|d| d.serial()),
Some(100)
);
assert!(store.get_delta_for_version(1, 200).unwrap().is_none());
assert_eq!(
store.get_delta_for_version(2, 300).unwrap().map(|d| d.serial()),
Some(300)
);
}
#[test]
fn store_db_save_cache_state_prunes_deltas_older_than_window_min() {
fn store_db_versioned_delta_window_wraparound_is_isolated_by_version() {
let dir = tempfile::tempdir().unwrap();
let store = RtrStore::open(dir.path()).unwrap();
let session_ids = SessionIds::from_array([40, 41, 42]);
let snapshot = Snapshot::from_payloads(vec![
Payload::RouteOrigin(v4_origin(192, 0, 2, 0, 24, 24, 64496)),
]);
let d101 = Delta::new(
101,
vec![Payload::RouteOrigin(v4_origin(192, 0, 2, 0, 24, 24, 64496))],
vec![],
);
let d102 = Delta::new(
102,
vec![Payload::RouteOrigin(v4_origin(198, 51, 100, 0, 24, 24, 64497))],
vec![],
);
let d103 = Delta::new(
103,
vec![Payload::RouteOrigin(v4_origin(203, 0, 113, 0, 24, 24, 64498))],
vec![],
);
store
.save_cache_state(
CacheAvailability::Ready,
&snapshot,
&session_ids,
101,
Some(&d101),
Some((101, 101)),
false,
)
.unwrap();
store
.save_cache_state(
CacheAvailability::Ready,
&snapshot,
&session_ids,
102,
Some(&d102),
Some((101, 102)),
false,
)
.unwrap();
store
.save_cache_state(
CacheAvailability::Ready,
&snapshot,
&session_ids,
103,
Some(&d103),
Some((103, 103)),
false,
)
.unwrap();
assert!(store.get_delta(101).unwrap().is_none());
assert!(store.get_delta(102).unwrap().is_none());
assert_eq!(store.get_delta(103).unwrap().map(|d| d.serial()), Some(103));
assert_eq!(store.get_delta_window().unwrap(), Some((103, 103)));
}
#[test]
fn store_db_load_delta_window_restores_wraparound_window_in_serial_order() {
let dir = tempfile::tempdir().unwrap();
let store = RtrStore::open(dir.path()).unwrap();
let session_ids = SessionIds::from_array([40, 41, 42]);
let snapshot = Snapshot::from_payloads(vec![
Payload::RouteOrigin(v4_origin(192, 0, 2, 0, 24, 24, 64496)),
]);
let snapshots = std::array::from_fn(|_| Snapshot::empty());
let session_ids = [600u16, 601u16, 602u16];
let serials = [0u32, 0u32, 0u32];
let d_max = Delta::new(
u32::MAX,
@@ -383,162 +122,228 @@ fn store_db_load_delta_window_restores_wraparound_window_in_serial_order() {
vec![Payload::RouteOrigin(v4_origin(203, 0, 113, 0, 24, 24, 64498))],
vec![],
);
store
.save_cache_state(
CacheAvailability::Ready,
&snapshot,
&session_ids,
u32::MAX,
Some(&d_max),
Some((u32::MAX, u32::MAX)),
false,
)
.unwrap();
store
.save_cache_state(
CacheAvailability::Ready,
&snapshot,
&session_ids,
0,
Some(&d_zero),
Some((u32::MAX, 0)),
false,
)
.unwrap();
store
.save_cache_state(
CacheAvailability::Ready,
&snapshot,
&session_ids,
1,
Some(&d_one),
Some((u32::MAX, 1)),
false,
)
.unwrap();
let loaded = store.load_delta_window(u32::MAX, 1).unwrap();
assert_eq!(loaded.iter().map(Delta::serial).collect::<Vec<_>>(), vec![u32::MAX, 0, 1]);
}
#[test]
fn store_db_load_snapshot_and_serial_returns_consistent_pair() {
let dir = tempfile::tempdir().unwrap();
let store = RtrStore::open(dir.path()).unwrap();
let snapshot = Snapshot::from_payloads(vec![
Payload::RouteOrigin(v4_origin(203, 0, 113, 0, 24, 24, 64498)),
]);
store.save_snapshot_and_serial(&snapshot, 200).unwrap();
let loaded = store
.load_snapshot_and_serial()
.unwrap()
.expect("snapshot+serial should exist");
let input = format!(
"db_path: {}\nsnapshot:\n{}serial=200\n",
dir.path().display(),
indent_block(&snapshot_to_string(&snapshot), 2),
);
let output = format!(
"loaded_snapshot:\n{}loaded_serial: {}\n",
indent_block(&snapshot_to_string(&loaded.0), 2),
loaded.1,
);
test_report(
"store_db_load_snapshot_and_serial_returns_consistent_pair",
"验证 load_snapshot_and_serial() 能正确返回一致的 snapshot 与 serial。",
&input,
&output,
);
assert!(snapshot.same_content(&loaded.0));
assert_eq!(loaded.1, 200);
}
#[test]
fn store_db_delete_snapshot_delta_and_serial_removes_data() {
let dir = tempfile::tempdir().unwrap();
let store = RtrStore::open(dir.path()).unwrap();
let snapshot = Snapshot::from_payloads(vec![
Payload::RouteOrigin(v4_origin(192, 0, 2, 0, 24, 24, 64496)),
]);
let delta = Delta::new(
101,
vec![Payload::RouteOrigin(v4_origin(198, 51, 100, 0, 24, 24, 64497))],
let d_v1_only = Delta::new(
0,
vec![Payload::RouteOrigin(v4_origin(10, 0, 0, 0, 24, 24, 64500))],
vec![],
);
store.save_snapshot(&snapshot).unwrap();
store.save_delta(&delta).unwrap();
store.set_serial(100).unwrap();
store
.save_cache_state_versioned(
CacheAvailability::Ready,
&snapshots,
&session_ids,
&serials,
&[None, None, Some(&d_max)],
&[None, None, None],
&[false, false, false],
)
.unwrap();
store
.save_cache_state_versioned(
CacheAvailability::Ready,
&snapshots,
&session_ids,
&serials,
&[None, None, Some(&d_zero)],
&[None, None, None],
&[false, false, false],
)
.unwrap();
store
.save_cache_state_versioned(
CacheAvailability::Ready,
&snapshots,
&session_ids,
&serials,
&[None, None, Some(&d_one)],
&[None, None, None],
&[false, false, false],
)
.unwrap();
store
.save_cache_state_versioned(
CacheAvailability::Ready,
&snapshots,
&session_ids,
&serials,
&[None, Some(&d_v1_only), None],
&[None, None, None],
&[false, false, false],
)
.unwrap();
store.delete_snapshot().unwrap();
store.delete_delta(101).unwrap();
store.delete_serial().unwrap();
let loaded_snapshot = store.get_snapshot().unwrap();
let loaded_delta = store.get_delta(101).unwrap();
let loaded_serial = store.get_serial().unwrap();
let input = format!(
"db_path: {}\nsave snapshot + delta(101) + serial(100), then delete all three.\n",
dir.path().display(),
let v2_loaded = store.load_delta_window_for_version(2, u32::MAX, 1).unwrap();
assert_eq!(
v2_loaded.iter().map(Delta::serial).collect::<Vec<_>>(),
vec![u32::MAX, 0, 1]
);
let output = format!(
"get_snapshot: {:?}\nget_delta(101): {:?}\nget_serial: {:?}\n",
loaded_snapshot.as_ref().map(|_| "Some(snapshot)"),
loaded_delta.as_ref().map(|_| "Some(delta)"),
loaded_serial,
);
test_report(
"store_db_delete_snapshot_delta_and_serial_removes_data",
"验证 delete_snapshot()/delete_delta()/delete_serial() 后,对应数据不再可读。",
&input,
&output,
);
assert!(loaded_snapshot.is_none());
assert!(loaded_delta.is_none());
assert!(loaded_serial.is_none());
let v1_loaded = store.load_delta_window_for_version(1, 0, 0).unwrap();
assert_eq!(v1_loaded.iter().map(Delta::serial).collect::<Vec<_>>(), vec![0]);
assert_eq!(v1_loaded[0].announced().len(), 1);
}
#[test]
fn store_db_load_snapshot_and_serial_errors_on_inconsistent_state() {
fn store_db_versioned_clear_window_affects_only_target_version() {
let dir = tempfile::tempdir().unwrap();
let store = RtrStore::open(dir.path()).unwrap();
let snapshot = Snapshot::from_payloads(vec![
Payload::RouteOrigin(v4_origin(192, 0, 2, 0, 24, 24, 64496)),
]);
store.save_snapshot(&snapshot).unwrap();
// 故意不写 serial,制造不一致状态
let result = store.load_snapshot_and_serial();
let input = format!(
"db_path: {}\n仅保存 snapshot,不保存 serial。\n",
dir.path().display(),
let snapshots = [
Snapshot::from_payloads(vec![Payload::RouteOrigin(v4_origin(
192, 0, 2, 0, 24, 24, 64496,
))]),
Snapshot::from_payloads(vec![Payload::RouteOrigin(v4_origin(
198, 51, 100, 0, 24, 24, 64497,
))]),
Snapshot::from_payloads(vec![Payload::RouteOrigin(v4_origin(
203, 0, 113, 0, 24, 24, 64498,
))]),
];
let session_ids = [420u16, 421u16, 422u16];
let serials = [10u32, 20u32, 30u32];
let d0 = Delta::new(
10,
vec![Payload::RouteOrigin(v4_origin(192, 0, 2, 0, 24, 24, 64496))],
vec![],
);
let d2 = Delta::new(
30,
vec![Payload::RouteOrigin(v4_origin(203, 0, 113, 0, 24, 24, 64498))],
vec![],
);
let output = format!("load_snapshot_and_serial result: {:?}\n", result);
store
.save_cache_state_versioned(
CacheAvailability::Ready,
&snapshots,
&session_ids,
&serials,
&[Some(&d0), None, Some(&d2)],
&[Some((10, 10)), None, Some((30, 30))],
&[false, false, false],
)
.unwrap();
test_report(
"store_db_load_snapshot_and_serial_errors_on_inconsistent_state",
"验证当 snapshot 和 serial 状态不一致时,load_snapshot_and_serial() 返回错误。",
&input,
&output,
store
.save_cache_state_versioned(
CacheAvailability::Ready,
&snapshots,
&session_ids,
&serials,
&[None, None, None],
&[None, None, None],
&[true, false, false],
)
.unwrap();
assert_eq!(store.get_delta_window_for_version(0).unwrap(), None);
assert!(store.get_delta_for_version(0, 10).unwrap().is_none());
assert_eq!(store.get_delta_window_for_version(2).unwrap(), Some((30, 30)));
assert_eq!(
store.get_delta_for_version(2, 30).unwrap().map(|d| d.serial()),
Some(30)
);
assert!(result.is_err());
}
#[test]
fn store_db_versioned_prunes_outside_window() {
let dir = tempfile::tempdir().unwrap();
let store = RtrStore::open(dir.path()).unwrap();
let snapshots = std::array::from_fn(|_| {
Snapshot::from_payloads(vec![Payload::RouteOrigin(v4_origin(
192, 0, 2, 0, 24, 24, 64496,
))])
});
let session_ids = [500u16, 501u16, 502u16];
let serials = [102u32, 0u32, 0u32];
let d100 = Delta::new(
100,
vec![Payload::RouteOrigin(v4_origin(10, 0, 0, 0, 24, 24, 65001))],
vec![],
);
let d101 = Delta::new(
101,
vec![Payload::RouteOrigin(v4_origin(10, 0, 1, 0, 24, 24, 65002))],
vec![],
);
let d102 = Delta::new(
102,
vec![Payload::RouteOrigin(v4_origin(10, 0, 2, 0, 24, 24, 65003))],
vec![],
);
store
.save_cache_state_versioned(
CacheAvailability::Ready,
&snapshots,
&session_ids,
&serials,
&[Some(&d100), None, None],
&[Some((100, 100)), None, None],
&[false, false, false],
)
.unwrap();
store
.save_cache_state_versioned(
CacheAvailability::Ready,
&snapshots,
&session_ids,
&serials,
&[Some(&d101), None, None],
&[Some((100, 101)), None, None],
&[false, false, false],
)
.unwrap();
store
.save_cache_state_versioned(
CacheAvailability::Ready,
&snapshots,
&session_ids,
&serials,
&[Some(&d102), None, None],
&[Some((102, 102)), None, None],
&[false, false, false],
)
.unwrap();
assert!(store.get_delta_for_version(0, 100).unwrap().is_none());
assert!(store.get_delta_for_version(0, 101).unwrap().is_none());
assert_eq!(
store.get_delta_for_version(0, 102).unwrap().map(|d| d.serial()),
Some(102)
);
}
#[test]
fn store_db_versioned_load_delta_window_requires_complete_range() {
let dir = tempfile::tempdir().unwrap();
let store = RtrStore::open(dir.path()).unwrap();
let snapshots = std::array::from_fn(|_| Snapshot::empty());
let session_ids = [700u16, 701u16, 702u16];
let serials = [0u32, 0u32, 0u32];
let d11 = Delta::new(
11,
vec![Payload::RouteOrigin(v4_origin(198, 51, 100, 0, 24, 24, 64497))],
vec![],
);
store
.save_cache_state_versioned(
CacheAvailability::Ready,
&snapshots,
&session_ids,
&serials,
&[None, Some(&d11), None],
&[None, None, None],
&[false, false, false],
)
.unwrap();
let err = store.load_delta_window_for_version(1, 10, 11).unwrap_err();
assert!(err
.to_string()
.contains("delta window starts at 10, but first persisted delta is Some(11)"));
}