Skip to content

Commit 637401a

Browse files
mraszykclaudeCopilot
authored
test(xnet): exercise silently dropped responses on subnet deletion (#10793)
The subnet-deletion XNet tests only checked that in-flight requests towards a deleted subnet are rejected. This PR also exercises that in-flight responses towards a deleted subnet are silently dropped (a response can never be turned into a reject). - Add a universal-canister op `LoopUntilGlobalDataSet` that loops on `canister_status` calls (requesting its own status) until its global data is set to a fixed blob, then replies — used to delay a response until a chosen moment. - Extend both the system test (`rs/tests/message_routing/xnet/subnet_delete_test.rs`) and its `StateMachine` analogue (`rs/state_machine_tests/tests/subnet_delete.rs`): the deleted subnet's canister calls two canisters on a surviving subnet whose replies are released so that one response ends up in the stream and the other in the sender's output queue when the subnet is deleted. - Verify the drops: the queued response via `mr_routed_message_count{type="response",status="canister_not_found"}`; the streamed response via the stream being discarded (metric-silent bulk drop). --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent 8416bac commit 637401a

8 files changed

Lines changed: 725 additions & 90 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rs/state_machine_tests/tests/subnet_delete.rs

Lines changed: 298 additions & 45 deletions
Large diffs are not rendered by default.

rs/tests/message_routing/xnet/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ system_test(
148148
runtime_deps = UNIVERSAL_CANISTER_RUNTIME_DEPS | NNS_CANISTER_RUNTIME_DEPS,
149149
deps = [
150150
# Keep sorted.
151+
"//rs/config",
151152
"//rs/nns/constants",
152153
"//rs/registry/canister",
153154
"//rs/registry/subnet_type",

rs/tests/message_routing/xnet/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ candid = { workspace = true }
1212
canister-test = { path = "../../../rust_canisters/canister_test" }
1313
futures = { workspace = true }
1414
ic-agent = { workspace = true }
15+
ic-config = { path = "../../../config" }
1516
ic-error-types = { path = "../../../../packages/ic-error-types" }
1617
ic-management-canister-types-private = { path = "../../../types/management_canister_types" }
1718
ic-nns-constants = { path = "../../../nns/constants" }

rs/tests/message_routing/xnet/subnet_delete_test.rs

Lines changed: 353 additions & 45 deletions
Large diffs are not rendered by default.

rs/universal_canister/impl/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,5 +126,6 @@ try_from_u8!(
126126
CostHttpRequestV2 = 97,
127127
MsgCallerInfoData = 98,
128128
MsgCallerInfoSigner = 99,
129+
LoopUntilGlobalDataSet = 100,
129130
}
130131
);

rs/universal_canister/impl/src/main.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,20 @@ pub struct TransformArg {
2828
pub context: Vec<u8>,
2929
}
3030

31+
/// Candid argument of the management canister's `canister_status` method.
32+
#[derive(CandidType, Deserialize)]
33+
pub struct CanisterIdRecord {
34+
pub canister_id: Principal,
35+
}
36+
37+
/// Encodes a `PushBytes` op (opcode byte, little-endian `u32` length, bytes)
38+
/// into `out`, matching the encoding produced by the `PayloadBuilder`.
39+
fn encode_push_bytes(out: &mut Vec<u8>, data: &[u8]) {
40+
out.push(Ops::PushBytes as u8);
41+
out.extend_from_slice(&(data.len() as u32).to_le_bytes());
42+
out.extend_from_slice(data);
43+
}
44+
3145
fn http_reply_with_body(body: &[u8]) -> Vec<u8> {
3246
Encode!(&HttpResponse {
3347
status: 200_u128,
@@ -513,6 +527,47 @@ fn eval(ops_bytes: OpsBytes) {
513527
core::arch::wasm32::memory_grow(0, pages as usize);
514528
}
515529
}
530+
Ops::LoopUntilGlobalDataSet => {
531+
// Pop in the reverse of the builder's push order (trigger then reply).
532+
let reply_data = stack.pop_blob();
533+
let trigger = stack.pop_blob();
534+
if get_global() == trigger {
535+
// The awaited condition holds: reply and stop looping.
536+
api::reply_data_append(&reply_data);
537+
api::reply();
538+
} else {
539+
// The condition does not hold yet: make a management-canister
540+
// `canister_status` call for this canister itself, and re-run
541+
// this same op from both its reply and reject callbacks. This
542+
// forms a loop across message boundaries that only ends once
543+
// some other message sets the global data to `trigger`,
544+
// thereby delaying the reply to the original caller.
545+
let mut cb = Vec::new();
546+
encode_push_bytes(&mut cb, &trigger);
547+
encode_push_bytes(&mut cb, &reply_data);
548+
cb.push(Ops::LoopUntilGlobalDataSet as u8);
549+
// A single call yields either a reply or a reject (never
550+
// both), so the same callback env can back both handlers.
551+
let env = add_callback(cb);
552+
let arg = Encode!(&CanisterIdRecord {
553+
canister_id: Principal::from_slice(&api::id()),
554+
})
555+
.unwrap();
556+
api::call_new(
557+
Principal::management_canister().as_slice(),
558+
b"canister_status",
559+
callback,
560+
env,
561+
callback,
562+
env,
563+
);
564+
api::call_data_append(&arg);
565+
let err_code = api::call_perform();
566+
if err_code != 0 {
567+
api::trap_with("call_perform failed in LoopUntilGlobalDataSet")
568+
}
569+
}
570+
}
516571
}
517572
}
518573
}

rs/universal_canister/lib/src/lib.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -847,6 +847,21 @@ impl PayloadBuilder {
847847
self
848848
}
849849

850+
/// Loops indefinitely — each iteration performing a management-canister
851+
/// `canister_status` call for the executing canister — until the canister's
852+
/// global data equals `trigger`, and then replies with `reply`.
853+
///
854+
/// Because each loop iteration is an inter-canister call, the reply to the
855+
/// caller is delayed across message boundaries until some other message
856+
/// sets the global data to `trigger` (e.g. via `set_global_data`). This is
857+
/// useful for delaying a response until an external condition is met.
858+
pub fn loop_until_global_data_set(mut self, trigger: &[u8], reply: &[u8]) -> Self {
859+
self = self.push_bytes(trigger);
860+
self = self.push_bytes(reply);
861+
self.0.push(Ops::LoopUntilGlobalDataSet as u8);
862+
self
863+
}
864+
850865
pub fn build(self) -> Vec<u8> {
851866
self.0
852867
}

0 commit comments

Comments
 (0)