Skip to content

feat: add support of new type of subnet (cloud engine) in domain canister matcher#181

Draft
shilingwang wants to merge 3 commits intomainfrom
shiling/engine
Draft

feat: add support of new type of subnet (cloud engine) in domain canister matcher#181
shilingwang wants to merge 3 commits intomainfrom
shiling/engine

Conversation

@shilingwang
Copy link
Contributor

#Node-1865

Summary

This PR replaces the hardcoded engine-subnet CLI flag with a dynamic,
periodically-refreshed view of the NNS routing table, and extends
DomainCanisterMatcher to route requests based on live subnet type
information fetched directly from the NNS state tree.

Motivation

Previously, whether a canister ran on an "engine subnet" was determined
by a static CLI flag. This meant operators had to know and manually
maintain the list of engine subnets. It also relied on a hardcoded
SYSTEM_SUBNETS range table for system subnet detection. Both are
fragile as the network evolves.

Changes

New: src/routing/ic/subnets_info.rs

Introduces SubnetsInfoFetcher, a background task (implements Run)
that periodically queries the NNS state tree in three round trips:

  1. /subnet — discovers all subnet IDs.
  2. /canister_ranges — fetches the full routing table (CBOR-encoded
    shards per subnet), decoded into a sorted Vec<(lo, hi, subnet_id)>
    for O(log n) lookups.
  3. /subnet/<id>/type (batched) — fetches the type for every
    discovered subnet.

The result is stored in SubnetsInfo behind an Arc<ArcSwap<...>> for
lock-free, thread-safe access on the hot path. SubnetsInfo::subnet_type(canister_id)
performs a binary-search over the sorted range table.

SubnetType covers all four types currently defined in the IC interface
spec: Application, System, VerifiedApplication, and the newly
added CloudEngine (per dfinity/ic#8892).

Updated: DomainCanisterMatcher

  • Removes the hardcoded SYSTEM_SUBNETS table and is_system_subnet
    function. System subnet detection now uses SubnetType::System from
    the live snapshot.
  • check() takes subnets_info: &SubnetsInfo as an explicit parameter
    (injected by the middleware layer) instead of holding an
    Arc<ArcSwap<SubnetsInfo>> internally — keeping the matcher as pure
    static config.
  • Routing logic is now a single match on subnet_type:
    • System → system domains
    • CloudEngine → engine domains
    • anything else (including unknown/empty snapshot) → app domains

Updated: CanisterMatcherState

Owns the Arc<ArcSwap<SubnetsInfo>>, loads the current snapshot once
per request, and passes it into check().

Updated: CLI (src/cli.rs)

  • --domain-engine: list of domains to serve cloud-engine subnet
    canisters from.
  • --subnets-info-poll-interval (default 5m): how often to refresh
    the NNS snapshot (replaces the engine-specific
    --domain-engine-poll-interval).

Updated: core.rs

SubnetsInfoFetcher is now always started as a background task (no
longer gated on --domain-engine being set), because system subnet
routing also depends on the live snapshot.

Testing

  • 8 new unit tests in domain_canister.rs covering all routing paths:
    system / engine / app canister on correct and incorrect domains, the
    pre-isolation canister bypass, and empty-snapshot fallback behaviour.
  • Existing integration tests and benchmarks continue to use
    SubnetsInfo::default() (empty snapshot → app-domain fallback), which
    is correct for their setup.

@shilingwang shilingwang marked this pull request as ready for review February 25, 2026 09:03
@shilingwang shilingwang requested a review from a team as a code owner February 25, 2026 09:03
@shilingwang shilingwang marked this pull request as draft February 26, 2026 14:56
let domains = match subnets_info.subnet_type(canister_id) {
Some(SubnetType::System) => &self.domains_system,
Some(SubnetType::CloudEngine) => &self.domains_engine,
_ => &self.domains_app,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a good practice to explicitly match (instead of _), so if somebody adds a new SubnetType, they get a compile-time error and won't forget to update this code.
You can use Some(SubnetType::...) | None to match two things at the same time.

impl SubnetType {
fn from_bytes(bytes: &[u8]) -> Option<Self> {
match bytes {
b"application" => Some(Self::Application),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where does the bytes representation come from?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These byte strings come directly from the NNS state tree. Specifically, they are the raw leaf values read from the path /subnet/<subnet_id>/type in the certified state tree. The NNS encodes the subnet type as a plain UTF-8 string in the tree, so what arrives over the wire is bytes like b"application", b"system", etc.

}
}

#[cfg(test)]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this will only be available in uni tests of this crate and not in the tests of any other crates. Probably good enough, just wanted to point out in case you need this method in other tests.

None => {
warn!(
"Unknown subnet type {:?} for subnet {subnet_id}",
std::str::from_utf8(type_bytes).unwrap_or("<invalid utf8>")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use from_utf8_lossy which replaces non-utf8 characters with ?, so the string is still somewhat readable.


/// Reads `/canister_ranges/<subnet_id>` from the NNS state tree for each
/// subnet and decodes all CBOR-encoded range chunks into a sorted routing
/// table.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider mentioning the return value and the fact that its sorted

mut canister_ranges: Vec<(Principal, Principal, Principal)>,
subnet_types: AHashMap<Principal, SubnetType>,
) -> Self {
canister_ranges.sort_unstable_by_key(|(lo, _, _)| *lo);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optional: Since fetch_canister_ranges also has to sort, you could consider constructing SubnetsInfo with new and always sort in new to ensure the invariant is upheld. Then you don't need the sort in the rest of the code.

// canister_ranges/<subnet_id>/<chunk_start_bytes> = <cbor blob>
let mut canister_ranges: Vec<(Principal, Principal, Principal)> = Vec::new();

for &subnet_id in subnet_ids {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't you want to do it in parallel?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants