Skip to content

Commit 85b2daf

Browse files
authored
refactor(registry): Use shards for get_subnet_for_canister (#5867)
This change allows get_subnet_for_canister to operate more efficiently, only loading the portion of the routing table needed to return a response, instead of all of the shards
1 parent f6b9d5a commit 85b2daf

File tree

2 files changed

+225
-12
lines changed

2 files changed

+225
-12
lines changed

rs/registry/canister/src/mutations/node_management/common.rs

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -258,41 +258,64 @@ pub fn node_exists_with_ipv4(registry: &Registry, ipv4_addr: &str) -> bool {
258258
/// Similar to `get_key_family` on the `RegistryClient`, return a list of
259259
/// tuples, (ID, value). This strips the prefix from the key and returns the
260260
/// value as a decoded struct.
261+
///
262+
/// This function must return keys in order of their bytes, which should
263+
/// also be the same order as the string representations.
261264
pub(crate) fn get_key_family<T: prost::Message + Default>(
262265
registry: &Registry,
263266
prefix: &str,
264267
) -> Vec<(String, T)> {
265268
get_key_family_iter(registry, prefix).collect()
266269
}
267270

271+
/// This function must return keys in order of their bytes, which should
272+
/// also be the same order as the string representations.
268273
pub(crate) fn get_key_family_iter<'a, T: prost::Message + Default>(
269274
registry: &'a Registry,
270275
prefix: &'a str,
271276
) -> impl Iterator<Item = (String, T)> + 'a {
272277
get_key_family_iter_at_version(registry, prefix, registry.latest_version())
273278
}
274279

280+
/// This function must return keys in order of their bytes, which should
281+
/// also be the same order as the string representations.
275282
pub(crate) fn get_key_family_iter_at_version<'a, T: prost::Message + Default>(
276283
registry: &'a Registry,
277284
prefix: &'a str,
278285
version: u64,
279286
) -> impl Iterator<Item = (String, T)> + 'a {
287+
get_key_family_raw_iter_at_version(registry, prefix, version).filter_map(|(id, value)| {
288+
let latest_value: Option<T> =
289+
with_chunks(|chunks| decode_high_capacity_registry_value::<T, _>(value, chunks));
290+
291+
let latest_value = latest_value?;
292+
293+
Some((id, latest_value))
294+
})
295+
}
296+
297+
/// This function must return keys in order of their bytes, which should
298+
/// also be the same order as the string representations.
299+
pub(crate) fn get_key_family_raw_iter_at_version<'a>(
300+
registry: &'a Registry,
301+
prefix: &'a str,
302+
version: u64,
303+
) -> impl Iterator<Item = (String, &'a HighCapacityRegistryValue)> + 'a {
280304
let prefix_bytes = prefix.as_bytes();
281305
let start = prefix_bytes.to_vec();
282306

307+
// Note, using the 'store' which is a BTreeMap is what guarantees the order of keys.
283308
registry
284309
.store
285310
.range(start..)
286311
.take_while(|(k, _)| k.starts_with(prefix_bytes))
287312
.filter_map(move |(key, values)| {
288-
let value: &HighCapacityRegistryValue =
313+
let latest_value: &HighCapacityRegistryValue =
289314
values.iter().rev().find(|value| value.version <= version)?;
290315

291-
let latest_value: Option<T> =
292-
with_chunks(|chunks| decode_high_capacity_registry_value::<T, _>(value, chunks));
293-
294-
// Skip deleted values.
295-
let latest_value = latest_value?;
316+
if !latest_value.is_present() {
317+
return None; // Deleted or otherwise empty value.
318+
}
296319

297320
let id = key
298321
.strip_prefix(prefix_bytes)

rs/registry/canister/src/mutations/routing_table.rs

Lines changed: 196 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1-
use crate::mutations::node_management::common::get_key_family_iter_at_version;
2-
use crate::{pb::v1::SubnetForCanister, registry::Registry};
1+
use crate::mutations::node_management::common::{
2+
get_key_family_iter_at_version, get_key_family_raw_iter_at_version,
3+
};
4+
use crate::storage::with_chunks;
5+
use crate::{common::LOG_PREFIX, pb::v1::SubnetForCanister, registry::Registry};
36
use dfn_core::CanisterId;
47
use ic_base_types::{PrincipalId, SubnetId};
58
use ic_protobuf::registry::routing_table::v1 as pb;
9+
use ic_registry_canister_chunkify::decode_high_capacity_registry_value;
610
use ic_registry_keys::{
711
make_canister_migrations_record_key, make_canister_ranges_key, make_routing_table_record_key,
812
CANISTER_RANGES_PREFIX,
@@ -254,6 +258,41 @@ impl Registry {
254258
RoutingTable::try_from(pb::RoutingTable { entries }).unwrap()
255259
}
256260

261+
pub fn get_routing_table_shard_for_canister_id(
262+
&self,
263+
canister_id: CanisterId,
264+
version: u64,
265+
) -> Result<RoutingTable, String> {
266+
// get_key_family_* functions don't return deleted values.
267+
let ranges: Vec<_> =
268+
get_key_family_raw_iter_at_version(self, CANISTER_RANGES_PREFIX, version)
269+
.map(|(k, v)| {
270+
let shard_canister_id_bytes = hex::decode(k).unwrap();
271+
(CanisterId::try_from(shard_canister_id_bytes).unwrap(), v)
272+
})
273+
.collect();
274+
275+
let maybe_range_to_decode = ranges
276+
.into_iter()
277+
.take_while(|(shard_canister_id, _)| shard_canister_id <= &canister_id)
278+
.last()
279+
.map(|(_, record)| record);
280+
281+
let Some(range_to_decode) = maybe_range_to_decode else {
282+
return Err(format!("{}Could not find routing table shard", LOG_PREFIX));
283+
};
284+
285+
match with_chunks(|chunks| {
286+
decode_high_capacity_registry_value::<pb::RoutingTable, _>(range_to_decode, chunks)
287+
}) {
288+
Some(rt) => RoutingTable::try_from(rt).map_err(|e| e.to_string()),
289+
None => Err(format!(
290+
"{}Could not decode routing table shard",
291+
LOG_PREFIX
292+
)),
293+
}
294+
}
295+
257296
/// Applies the given mutation to the routing table at the specified version.
258297
fn modify_routing_table(
259298
&self,
@@ -388,11 +427,14 @@ impl Registry {
388427
principal_id: &PrincipalId,
389428
) -> Result<SubnetForCanister, GetSubnetForCanisterError> {
390429
let latest_version = self.latest_version();
391-
let routing_table = self.get_routing_table_or_panic(latest_version);
392-
let canister_id = CanisterId::try_from(*principal_id)
430+
431+
let canister_id = CanisterId::try_from_principal_id(*principal_id)
393432
.map_err(|_| GetSubnetForCanisterError::InvalidCanisterId)?;
433+
let routing_table_segment = self
434+
.get_routing_table_shard_for_canister_id(canister_id, latest_version)
435+
.map_err(|_| GetSubnetForCanisterError::NoSubnetAssigned)?;
394436

395-
match routing_table
437+
match routing_table_segment
396438
.lookup_entry(canister_id)
397439
.map(|(_, subnet_id)| subnet_id.get())
398440
{
@@ -460,6 +502,149 @@ mod tests {
460502
// GetSubnetForCanisterError::CanisterIdConversion currently not reachable - CanisterId::try_from() always succeeds
461503
}
462504

505+
// Helper functions to reduce test boilerplate
506+
fn assert_subnet_for_canister(registry: &Registry, canister_id: u64, expected_subnet_idx: u64) {
507+
let result = registry
508+
.get_subnet_for_canister(&CanisterId::from(canister_id).get())
509+
.unwrap_or_else(|_| panic!("No subnet found for canister {}", canister_id));
510+
511+
assert_eq!(
512+
result.subnet_id.unwrap(),
513+
PrincipalId::new_subnet_test_id(expected_subnet_idx),
514+
"Canister {} should be on subnet with index {}",
515+
canister_id,
516+
expected_subnet_idx
517+
);
518+
}
519+
520+
// Helper function to assert that no subnet is assigned for a canister
521+
fn assert_no_subnet_for_canister(registry: &Registry, canister_id: u64) {
522+
assert_matches!(
523+
registry
524+
.get_subnet_for_canister(&CanisterId::from(canister_id).get())
525+
.unwrap_err(),
526+
GetSubnetForCanisterError::NoSubnetAssigned
527+
);
528+
}
529+
530+
#[test]
531+
fn get_subnet_for_canister_with_multiple_shards() {
532+
let mut registry = invariant_compliant_registry(0);
533+
// Create enough entries to trigger multiple shards (more than MAX_RANGES_PER_CANISTER_RANGES)
534+
let entries = make_rt_entry_definitions(0..25, 100); // 25 entries, each covering 100 canister IDs
535+
536+
let rt = rt_from_ranges(entries);
537+
538+
let mutations = routing_table_into_registry_mutation(&registry, rt);
539+
registry.maybe_apply_mutation_internal(mutations);
540+
541+
// Test 5 random canister IDs from different ranges
542+
// Test canister ID 50 (should be in range 0-99, subnet_id from index 0)
543+
assert_subnet_for_canister(&registry, 50, 0);
544+
545+
// Test canister ID 550 (should be in range 500-599, subnet_id from index 5)
546+
assert_subnet_for_canister(&registry, 550, 5);
547+
548+
// Test canister ID 1250 (should be in range 1200-1299, subnet_id from index 12)
549+
assert_subnet_for_canister(&registry, 1250, 12);
550+
551+
// Test canister ID 1999 (should be in range 1900-1999, subnet_id from index 19)
552+
assert_subnet_for_canister(&registry, 1999, 19);
553+
554+
// Test canister ID 2399 (should be in range 2300-2399, subnet_id from index 23)
555+
assert_subnet_for_canister(&registry, 2399, 23);
556+
557+
// Test a canister ID outside all ranges
558+
assert_no_subnet_for_canister(&registry, 3000);
559+
}
560+
561+
#[test]
562+
fn test_canister_lookup_at_shard_boundaries() {
563+
let mut registry = invariant_compliant_registry(0);
564+
let shards = shards(vec![
565+
(0, vec![((0, 999), test_subnet(0))]),
566+
(1000, vec![((1000, 1999), test_subnet(1))]),
567+
(2000, vec![((2000, 2999), test_subnet(2))]),
568+
]);
569+
apply_shards_to_registry(&mut registry, &shards);
570+
571+
assert_subnet_for_canister(&registry, 1000, 1);
572+
assert_subnet_for_canister(&registry, 999, 0);
573+
assert_subnet_for_canister(&registry, 1999, 1);
574+
assert_subnet_for_canister(&registry, 2000, 2);
575+
assert_no_subnet_for_canister(&registry, 3000);
576+
}
577+
578+
#[test]
579+
fn test_canister_lookup_with_gaps_in_shards() {
580+
let mut registry = invariant_compliant_registry(0);
581+
let shards = shards(vec![
582+
(0, vec![((0, 500), test_subnet(0))]),
583+
(2000, vec![((2000, 2500), test_subnet(2))]),
584+
]);
585+
apply_shards_to_registry(&mut registry, &shards);
586+
587+
assert_subnet_for_canister(&registry, 250, 0);
588+
assert_no_subnet_for_canister(&registry, 1500);
589+
assert_subnet_for_canister(&registry, 2250, 2);
590+
}
591+
592+
#[test]
593+
fn test_canister_lookup_with_empty_shards() {
594+
let mut registry = invariant_compliant_registry(0);
595+
let shards = shards(vec![
596+
(0, vec![((0, 500), test_subnet(0))]),
597+
(1000, vec![]), // Empty shard
598+
(2000, vec![((2000, 2500), test_subnet(2))]),
599+
]);
600+
apply_shards_to_registry(&mut registry, &shards);
601+
602+
assert_no_subnet_for_canister(&registry, 1500);
603+
}
604+
605+
#[test]
606+
fn test_canister_lookup_below_first_shard() {
607+
let mut registry = invariant_compliant_registry(0);
608+
let shards = shards(vec![
609+
(1000, vec![((1000, 1500), test_subnet(1))]),
610+
(2000, vec![((2000, 2500), test_subnet(2))]),
611+
]);
612+
apply_shards_to_registry(&mut registry, &shards);
613+
614+
assert_no_subnet_for_canister(&registry, 500);
615+
}
616+
617+
#[test]
618+
fn test_canister_lookup_with_gaps() {
619+
let mut registry = invariant_compliant_registry(0);
620+
let shards = shards(vec![
621+
(0, vec![((100, 500), test_subnet(0))]),
622+
(400, vec![((600, 800), test_subnet(1))]),
623+
(850, vec![((900, 1200), test_subnet(2))]),
624+
]);
625+
apply_shards_to_registry(&mut registry, &shards);
626+
627+
assert_subnet_for_canister(&registry, 300, 0);
628+
assert_no_subnet_for_canister(&registry, 50);
629+
assert_subnet_for_canister(&registry, 700, 1);
630+
}
631+
632+
#[test]
633+
fn test_canister_lookup_at_max_canister_id() {
634+
let mut registry = invariant_compliant_registry(0);
635+
let shards = shards(vec![
636+
(0, vec![((0, 1000), test_subnet(0))]),
637+
(
638+
u64::MAX - 1000,
639+
vec![((u64::MAX - 1000, u64::MAX), test_subnet(1))],
640+
),
641+
]);
642+
apply_shards_to_registry(&mut registry, &shards);
643+
644+
assert_subnet_for_canister(&registry, u64::MAX, 1);
645+
assert_no_subnet_for_canister(&registry, u64::MAX - 2000);
646+
}
647+
463648
#[test]
464649
fn test_routing_table_updates_and_deletes_entries_as_expected() {
465650
let mut registry = invariant_compliant_registry(0);
@@ -516,6 +701,11 @@ mod tests {
516701
);
517702
}
518703

704+
// Simple helper to create SubnetId from index
705+
fn test_subnet(idx: u64) -> SubnetId {
706+
SubnetId::new(PrincipalId::new_subnet_test_id(idx))
707+
}
708+
519709
/// Helper to build a RoutingTable from a Vec of ((start, end), subnet_id)
520710
fn rt_from_ranges(ranges: Vec<((u64, u64), SubnetId)>) -> RoutingTable {
521711
let pb_rt = shard_pb_rt(ranges);
@@ -587,7 +777,7 @@ mod tests {
587777
i * entries_per_range,
588778
i * entries_per_range + entries_per_range - 1,
589779
),
590-
SubnetId::new(PrincipalId::new_user_test_id(i)),
780+
SubnetId::new(PrincipalId::new_subnet_test_id(i)),
591781
)
592782
})
593783
.collect()

0 commit comments

Comments
 (0)