Skip to content

Commit 782c748

Browse files
max-dfinityIDX GitHub Automation
andauthored
feat(registry): Add invariant check for sharded canister migrations (#5535)
This changes the routing table invariant check to make sure there is a valid routing table built from shards, and that it is equal to the single record routing table. Additionally, it adds a benchmark for the routing table invariant check. --------- Co-authored-by: IDX GitHub Automation <infra+github-automation@dfinity.org>
1 parent 5ec9d81 commit 782c748

File tree

6 files changed

+132
-24
lines changed

6 files changed

+132
-24
lines changed

rs/registry/canister/canbench/canbench_results.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
benches:
2+
measure_routing_table_invariant_checks_shards_and_unsharded:
3+
total:
4+
calls: 1
5+
instructions: 563027802
6+
heap_increase: 69
7+
stable_memory_increase: 0
8+
scopes: {}
29
measure_snapshot_creation_with_1000_individual_entries:
310
total:
411
calls: 1

rs/registry/canister/src/flags.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::cell::Cell;
22

3-
#[cfg(test)]
3+
#[cfg(any(test, feature = "canbench-rs"))]
44
use ic_nervous_system_temporary::Temporary;
55

66
thread_local! {
@@ -11,7 +11,7 @@ pub(crate) fn is_chunkifying_large_values_enabled() -> bool {
1111
IS_CHUNKIFYING_LARGE_VALUES_ENABLED.get()
1212
}
1313

14-
#[cfg(test)]
14+
#[cfg(any(test, feature = "canbench-rs"))]
1515
pub fn temporarily_enable_chunkifying_large_values() -> Temporary {
1616
Temporary::new(&IS_CHUNKIFYING_LARGE_VALUES_ENABLED, true)
1717
}

rs/registry/canister/src/invariants/checks/benches.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
use crate::flags::temporarily_enable_chunkifying_large_values;
2+
use crate::invariants::routing_table::check_routing_table_invariants;
13
use crate::registry::Registry;
24
use canbench_rs::{bench, bench_fn, BenchResult};
35
use ic_base_types::{CanisterId, PrincipalId, SubnetId};
46
use ic_protobuf::registry::routing_table::v1::routing_table::Entry;
57
use ic_protobuf::registry::routing_table::v1::RoutingTable;
6-
use ic_registry_keys::make_canister_ranges_key;
8+
use ic_registry_keys::{make_canister_ranges_key, make_routing_table_record_key};
79
use ic_registry_routing_table::CanisterIdRange;
810
use ic_registry_transport::upsert;
911
use prost::Message;
@@ -90,3 +92,22 @@ fn measure_snapshot_creation_with_1_segment_of_1000_entries() -> BenchResult {
9092
fn measure_snapshot_creation_with_100_segments_of_1000_entries() -> BenchResult {
9193
benchmark_snapshot_creation_with_entries(100, 1000)
9294
}
95+
96+
/// 20k entries costs 250m instructions to validate
97+
#[bench(raw)]
98+
fn measure_routing_table_invariant_checks_shards_and_unsharded() -> BenchResult {
99+
let _feature = temporarily_enable_chunkifying_large_values();
100+
let mut registry = setup_registry_with_rt_segments_with_x_entries_each(1000, 20);
101+
102+
let rt =
103+
registry.get_routing_table_from_canister_range_records_or_panic(registry.latest_version());
104+
let rt_mutation = upsert(
105+
make_routing_table_record_key(),
106+
RoutingTable::from(rt).encode_to_vec(),
107+
);
108+
registry.apply_mutations_for_test(vec![rt_mutation]);
109+
110+
let snapshot = registry.take_latest_snapshot();
111+
112+
bench_fn(|| check_routing_table_invariants(&snapshot))
113+
}

rs/registry/canister/src/invariants/routing_table.rs

Lines changed: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ use crate::invariants::common::{InvariantCheckError, RegistrySnapshot};
22

33
use std::convert::TryFrom;
44

5+
use ic_base_types::CanisterId;
56
use ic_protobuf::registry::routing_table::v1::{
67
CanisterMigrations as pbCanisterMigrations, RoutingTable as pbRoutingTable,
78
};
8-
use ic_registry_keys::{make_canister_migrations_record_key, make_routing_table_record_key};
9+
use ic_registry_keys::{
10+
make_canister_migrations_record_key, make_canister_ranges_key, make_routing_table_record_key,
11+
};
912
use ic_registry_routing_table::{CanisterMigrations, RoutingTable};
1013
use prost::Message;
1114

@@ -19,14 +22,40 @@ pub(crate) fn check_routing_table_invariants(
1922

2023
// Return routing table from snapshot
2124
fn get_routing_table(snapshot: &RegistrySnapshot) -> RoutingTable {
22-
match snapshot.get(make_routing_table_record_key().as_bytes()) {
23-
Some(routing_table_bytes) => {
24-
let routing_table_proto =
25-
pbRoutingTable::decode(routing_table_bytes.as_slice()).unwrap();
26-
RoutingTable::try_from(routing_table_proto).unwrap()
27-
}
28-
None => panic!("No routing table in snapshot"),
25+
// TODO(NNS1-3781): Remove this once we have sharded table supported by all clients.
26+
let rt_from_routing_table_record =
27+
match snapshot.get(make_routing_table_record_key().as_bytes()) {
28+
Some(routing_table_bytes) => {
29+
let routing_table_proto =
30+
pbRoutingTable::decode(routing_table_bytes.as_slice()).unwrap();
31+
RoutingTable::try_from(routing_table_proto).unwrap()
32+
}
33+
None => panic!("No routing table in snapshot"),
34+
};
35+
36+
// If there are shards, they should match the routing table record.
37+
let shards = get_routing_table_shards(snapshot);
38+
if !shards.is_empty() {
39+
let rt_from_shards = RoutingTable::try_from(shards).unwrap();
40+
assert_eq!(
41+
rt_from_shards, rt_from_routing_table_record,
42+
"Routing tables from shards and routing table record do not match."
43+
);
44+
}
45+
46+
rt_from_routing_table_record
47+
}
48+
49+
fn get_routing_table_shards(snapshot: &RegistrySnapshot) -> Vec<pbRoutingTable> {
50+
let start = make_canister_ranges_key(CanisterId::from_u64(0)).into_bytes();
51+
let end = make_canister_ranges_key(CanisterId::from_u64(u64::MAX)).into_bytes();
52+
let mut shards = vec![];
53+
for (_, value) in snapshot.range(start..=end) {
54+
let routing_table_proto = pbRoutingTable::decode(value.as_slice()).unwrap();
55+
shards.push(routing_table_proto);
2956
}
57+
58+
shards
3059
}
3160

3261
/// Iff `canister_migrations` is present, check that its invariants hold if reading and conversion succeed.
@@ -291,4 +320,35 @@ mod tests {
291320
assert!(check_routing_table_invariants(&snapshot).is_ok());
292321
assert!(check_canister_migrations_invariants(&snapshot).is_err());
293322
}
323+
324+
#[test]
325+
#[should_panic(expected = "Routing tables from shards and routing table record do not match.")]
326+
fn if_sharded_ranges_they_must_match_original_routing_table() {
327+
let mut snapshot = RegistrySnapshot::new();
328+
329+
// The routing table before canister migration.
330+
let routing_table = RoutingTable::try_from(btreemap! {
331+
CanisterIdRange{ start: CanisterId::from(0x0), end: CanisterId::from(0xff) } => subnet_test_id(1),
332+
CanisterIdRange{ start: CanisterId::from(0x100), end: CanisterId::from(0x1ff) } => subnet_test_id(2),
333+
CanisterIdRange{ start: CanisterId::from(0x200), end: CanisterId::from(0x2ff) } => subnet_test_id(3),
334+
}).unwrap();
335+
336+
let routing_table = PbRoutingTable::from(routing_table);
337+
let key1 = make_routing_table_record_key();
338+
let value1 = routing_table.encode_to_vec();
339+
340+
let rt_shard = RoutingTable::try_from(btreemap! {
341+
CanisterIdRange{ start: CanisterId::from(0x0), end: CanisterId::from(0xff) } => subnet_test_id(1),
342+
CanisterIdRange{ start: CanisterId::from(0x100), end: CanisterId::from(0x1ff) } => subnet_test_id(2),
343+
}).unwrap();
344+
345+
let rt_shard = PbRoutingTable::from(rt_shard);
346+
let key2 = make_canister_ranges_key(CanisterId::from_u64(0x0));
347+
let value2 = rt_shard.encode_to_vec();
348+
349+
snapshot.insert(key1.into_bytes(), value1);
350+
snapshot.insert(key2.into_bytes(), value2);
351+
352+
check_routing_table_invariants(&snapshot).unwrap();
353+
}
294354
}

rs/registry/canister/unreleased_changelog.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ on the process that this file is part of, see
2525
[announced at the end of March in a forum][chunking] (and various other
2626
channels). This release marks the end of the "migration window" described in
2727
the aforelinked forum post.
28+
* The `check_routing_table_invariants` method now checks the new canister_ranges_
29+
and ensures they match the `routing_table` record. The old invariant check will be
30+
removed once `routing_table` is removed.
2831

2932
[chunking]: https://forum.dfinity.org/t/breaking-registry-changes-for-large-records/42893?u=daniel-wong
3033

rs/registry/routing_table/src/proto.rs

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -78,24 +78,41 @@ impl TryFrom<pb::RoutingTable> for RoutingTable {
7878
type Error = ProxyDecodeError;
7979

8080
fn try_from(src: pb::RoutingTable) -> Result<Self, Self::Error> {
81-
let mut map = BTreeMap::new();
82-
for entry in src.entries {
83-
let range = try_from_option_field(entry.range, "RoutingTable::Entry::range")?;
84-
let subnet_id =
85-
try_from_option_field(entry.subnet_id, "RoutingTable::Entry::subnet_id")?;
86-
let subnet_id = subnet_id_try_from_protobuf(subnet_id)?;
87-
if let Some(prev_subnet_id) = map.insert(range, subnet_id) {
88-
return Err(ProxyDecodeError::DuplicateEntry {
89-
key: format!("{:?}", range),
90-
v1: prev_subnet_id.to_string(),
91-
v2: subnet_id.to_string(),
92-
});
93-
}
81+
let entries_count = src.entries.len();
82+
let map: BTreeMap<_, _> = src
83+
.entries
84+
.into_iter()
85+
.map(|entry| {
86+
let range = try_from_option_field(entry.range, "RoutingTable::Entry::range")?;
87+
let subnet_id =
88+
try_from_option_field(entry.subnet_id, "RoutingTable::Entry::subnet_id")?;
89+
let subnet_id = subnet_id_try_from_protobuf(subnet_id)?;
90+
91+
Ok((range, subnet_id))
92+
})
93+
.collect::<Result<_, Self::Error>>()?;
94+
95+
if map.len() != entries_count {
96+
let diff = entries_count.saturating_sub(map.len());
97+
return Err(ProxyDecodeError::Other(format!(
98+
"There were {} duplicate entries in the routing table",
99+
diff
100+
)));
94101
}
102+
95103
Ok(map.try_into()?)
96104
}
97105
}
98106

107+
impl TryFrom<Vec<pb::RoutingTable>> for RoutingTable {
108+
type Error = ProxyDecodeError;
109+
110+
fn try_from(src: Vec<pb::RoutingTable>) -> Result<Self, Self::Error> {
111+
let entries = src.into_iter().flat_map(|table| table.entries).collect();
112+
Self::try_from(pb::RoutingTable { entries })
113+
}
114+
}
115+
99116
impl From<CanisterMigrations> for pb::CanisterMigrations {
100117
fn from(src: CanisterMigrations) -> Self {
101118
Self::from(&src)

0 commit comments

Comments
 (0)