|
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}; |
3 | 6 | use dfn_core::CanisterId;
|
4 | 7 | use ic_base_types::{PrincipalId, SubnetId};
|
5 | 8 | use ic_protobuf::registry::routing_table::v1 as pb;
|
| 9 | +use ic_registry_canister_chunkify::decode_high_capacity_registry_value; |
6 | 10 | use ic_registry_keys::{
|
7 | 11 | make_canister_migrations_record_key, make_canister_ranges_key, make_routing_table_record_key,
|
8 | 12 | CANISTER_RANGES_PREFIX,
|
@@ -254,6 +258,41 @@ impl Registry {
|
254 | 258 | RoutingTable::try_from(pb::RoutingTable { entries }).unwrap()
|
255 | 259 | }
|
256 | 260 |
|
| 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 | + |
257 | 296 | /// Applies the given mutation to the routing table at the specified version.
|
258 | 297 | fn modify_routing_table(
|
259 | 298 | &self,
|
@@ -388,11 +427,14 @@ impl Registry {
|
388 | 427 | principal_id: &PrincipalId,
|
389 | 428 | ) -> Result<SubnetForCanister, GetSubnetForCanisterError> {
|
390 | 429 | 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) |
393 | 432 | .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)?; |
394 | 436 |
|
395 |
| - match routing_table |
| 437 | + match routing_table_segment |
396 | 438 | .lookup_entry(canister_id)
|
397 | 439 | .map(|(_, subnet_id)| subnet_id.get())
|
398 | 440 | {
|
@@ -460,6 +502,149 @@ mod tests {
|
460 | 502 | // GetSubnetForCanisterError::CanisterIdConversion currently not reachable - CanisterId::try_from() always succeeds
|
461 | 503 | }
|
462 | 504 |
|
| 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(®istry, 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(®istry, 50, 0); |
| 544 | + |
| 545 | + // Test canister ID 550 (should be in range 500-599, subnet_id from index 5) |
| 546 | + assert_subnet_for_canister(®istry, 550, 5); |
| 547 | + |
| 548 | + // Test canister ID 1250 (should be in range 1200-1299, subnet_id from index 12) |
| 549 | + assert_subnet_for_canister(®istry, 1250, 12); |
| 550 | + |
| 551 | + // Test canister ID 1999 (should be in range 1900-1999, subnet_id from index 19) |
| 552 | + assert_subnet_for_canister(®istry, 1999, 19); |
| 553 | + |
| 554 | + // Test canister ID 2399 (should be in range 2300-2399, subnet_id from index 23) |
| 555 | + assert_subnet_for_canister(®istry, 2399, 23); |
| 556 | + |
| 557 | + // Test a canister ID outside all ranges |
| 558 | + assert_no_subnet_for_canister(®istry, 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(®istry, 1000, 1); |
| 572 | + assert_subnet_for_canister(®istry, 999, 0); |
| 573 | + assert_subnet_for_canister(®istry, 1999, 1); |
| 574 | + assert_subnet_for_canister(®istry, 2000, 2); |
| 575 | + assert_no_subnet_for_canister(®istry, 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(®istry, 250, 0); |
| 588 | + assert_no_subnet_for_canister(®istry, 1500); |
| 589 | + assert_subnet_for_canister(®istry, 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(®istry, 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(®istry, 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(®istry, 300, 0); |
| 628 | + assert_no_subnet_for_canister(®istry, 50); |
| 629 | + assert_subnet_for_canister(®istry, 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(®istry, u64::MAX, 1); |
| 645 | + assert_no_subnet_for_canister(®istry, u64::MAX - 2000); |
| 646 | + } |
| 647 | + |
463 | 648 | #[test]
|
464 | 649 | fn test_routing_table_updates_and_deletes_entries_as_expected() {
|
465 | 650 | let mut registry = invariant_compliant_registry(0);
|
@@ -516,6 +701,11 @@ mod tests {
|
516 | 701 | );
|
517 | 702 | }
|
518 | 703 |
|
| 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 | + |
519 | 709 | /// Helper to build a RoutingTable from a Vec of ((start, end), subnet_id)
|
520 | 710 | fn rt_from_ranges(ranges: Vec<((u64, u64), SubnetId)>) -> RoutingTable {
|
521 | 711 | let pb_rt = shard_pb_rt(ranges);
|
@@ -587,7 +777,7 @@ mod tests {
|
587 | 777 | i * entries_per_range,
|
588 | 778 | i * entries_per_range + entries_per_range - 1,
|
589 | 779 | ),
|
590 |
| - SubnetId::new(PrincipalId::new_user_test_id(i)), |
| 780 | + SubnetId::new(PrincipalId::new_subnet_test_id(i)), |
591 | 781 | )
|
592 | 782 | })
|
593 | 783 | .collect()
|
|
0 commit comments