Skip to content

Commit 24b22b8

Browse files
feat(pocket-ic): support custom blockmaker metrics at each round (dfinity#3685)
Currently PocketIC at each tick() executes a [round](https://github.com/dfinity/ic/blob/master/rs/state_machine_tests/src/lib.rs#L1338) with state_machine_tests. However a dummy [blockmaker](https://github.com/dfinity/ic/blob/0468e6f90e52fab4be7b0bf722dacb520a589c55/rs/types/types/src/batch.rs#L199-L200) is used. The metrics_collector_canister I am building store and serve node metrics from all the subnet. To test this canister I need then to modify PocketIC so that at each "round" it stores blockmaker metrics that I can pass in from tests --------- Co-authored-by: mraszyk <[email protected]>
1 parent a9c6652 commit 24b22b8

File tree

12 files changed

+412
-113
lines changed

12 files changed

+412
-113
lines changed

Cargo.lock

Lines changed: 101 additions & 97 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/pocket-ic/src/common/rest.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,23 @@ impl From<Principal> for RawNodeId {
356356
}
357357
}
358358

359+
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema, Default)]
360+
pub struct TickConfigs {
361+
pub blockmakers: Option<BlockmakerConfigs>,
362+
}
363+
364+
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
365+
pub struct BlockmakerConfigs {
366+
pub blockmakers_per_subnet: Vec<RawSubnetBlockmaker>,
367+
}
368+
369+
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
370+
pub struct RawSubnetBlockmaker {
371+
pub subnet: RawSubnetId,
372+
pub blockmaker: RawNodeId,
373+
pub failed_blockmakers: Vec<RawNodeId>,
374+
}
375+
359376
#[derive(Serialize, Deserialize, JsonSchema)]
360377
pub struct RawVerifyCanisterSigArg {
361378
#[serde(deserialize_with = "base64::deserialize")]

packages/pocket-ic/src/lib.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,14 @@ impl PocketIc {
522522
runtime.block_on(async { self.pocket_ic.tick().await })
523523
}
524524

525+
/// Make the IC produce and progress by one block with custom
526+
/// configs for the round.
527+
#[instrument(skip(self), fields(instance_id=self.pocket_ic.instance_id))]
528+
pub fn tick_with_configs(&self, configs: crate::common::rest::TickConfigs) {
529+
let runtime = self.runtime.clone();
530+
runtime.block_on(async { self.pocket_ic.tick_with_configs(configs).await })
531+
}
532+
525533
/// Configures the IC to make progress automatically,
526534
/// i.e., periodically update the time of the IC
527535
/// to the real time and execute rounds on the subnets.

packages/pocket-ic/src/nonblocking.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use crate::common::rest::{
55
MockCanisterHttpResponse, RawAddCycles, RawCanisterCall, RawCanisterHttpRequest, RawCanisterId,
66
RawCanisterResult, RawCycles, RawEffectivePrincipal, RawIngressStatusArgs, RawMessageId,
77
RawMockCanisterHttpResponse, RawPrincipalId, RawSetStableMemory, RawStableMemory, RawSubnetId,
8-
RawTime, RawVerifyCanisterSigArg, SubnetId, Topology,
8+
RawTime, RawVerifyCanisterSigArg, SubnetId, TickConfigs, Topology,
99
};
1010
use crate::management_canister::{
1111
CanisterId, CanisterIdRecord, CanisterInstallMode, CanisterInstallModeUpgradeInner,
@@ -290,8 +290,15 @@ impl PocketIc {
290290
/// inter-canister calls or heartbeats.
291291
#[instrument(skip(self), fields(instance_id=self.instance_id))]
292292
pub async fn tick(&self) {
293+
self.tick_with_configs(TickConfigs::default()).await;
294+
}
295+
296+
/// Make the IC produce and progress by one block with custom
297+
/// configs for the round.
298+
#[instrument(skip(self), fields(instance_id=self.instance_id))]
299+
pub async fn tick_with_configs(&self, configs: TickConfigs) {
293300
let endpoint = "update/tick";
294-
self.post::<(), _>(endpoint, "").await;
301+
self.post::<(), _>(endpoint, configs).await;
295302
}
296303

297304
/// Configures the IC to make progress automatically,

packages/pocket-ic/test_canister/canister.did

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,12 +98,29 @@ type TransformArgs = record {
9898
context : blob;
9999
};
100100

101+
type NodeMetricsHistoryArgs = record {
102+
subnet_id: principal;
103+
start_at_timestamp_nanos: nat64;
104+
};
105+
106+
type NodeMetrics = record {
107+
node_id: principal;
108+
num_blocks_proposed_total: nat64;
109+
num_block_failures_total: nat64;
110+
};
111+
112+
type NodeMetricsHistoryResponse = record {
113+
timestamp_nanos: nat64;
114+
node_metrics: vec NodeMetrics;
115+
};
116+
101117
service : {
102118
http_request: (request: HttpGatewayRequest) -> (HttpGatewayResponse) query;
103119
schnorr_public_key : (opt principal, vec blob, SchnorrKeyId) -> (SchnorrPublicKeyResult);
104120
sign_with_schnorr : (blob, vec blob, SchnorrKeyId) -> (SignWithSchnorrResult);
105121
ecdsa_public_key : (opt principal, vec blob, text) -> (EcdsaPublicKeyResult);
106122
sign_with_ecdsa : (blob, vec blob, text) -> (SignWithEcdsaResult);
123+
node_metrics_history_proxy: (NodeMetricsHistoryArgs) -> (vec NodeMetricsHistoryResponse);
107124
canister_http : () -> (HttpResponseResult);
108125
canister_http_with_transform : () -> (HttpResponse);
109126
transform : (TransformArgs) -> (HttpResponse) query;

packages/pocket-ic/test_canister/src/canister.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,40 @@ async fn call_with_large_blob(canister: Principal, blob_len: usize) -> usize {
293293
.0
294294
}
295295

296+
#[derive(CandidType, Deserialize)]
297+
pub struct NodeMetrics {
298+
pub node_id: Principal,
299+
pub num_blocks_proposed_total: u64,
300+
pub num_block_failures_total: u64,
301+
}
302+
303+
#[derive(CandidType, Deserialize)]
304+
pub struct NodeMetricsHistoryResponse {
305+
pub timestamp_nanos: u64,
306+
pub node_metrics: Vec<NodeMetrics>,
307+
}
308+
309+
#[derive(CandidType, Deserialize)]
310+
pub struct NodeMetricsHistoryArgs {
311+
pub start_at_timestamp_nanos: u64,
312+
pub subnet_id: Principal,
313+
}
314+
315+
#[update]
316+
async fn node_metrics_history_proxy(
317+
args: NodeMetricsHistoryArgs,
318+
) -> Vec<NodeMetricsHistoryResponse> {
319+
ic_cdk::api::call::call_with_payment128::<_, (Vec<NodeMetricsHistoryResponse>,)>(
320+
candid::Principal::management_canister(),
321+
"node_metrics_history",
322+
(args,),
323+
0_u128,
324+
)
325+
.await
326+
.unwrap()
327+
.0
328+
}
329+
296330
// executing many instructions
297331

298332
#[update]

packages/pocket-ic/tests/tests.rs

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ use candid::{decode_one, encode_one, CandidType, Decode, Deserialize, Encode, Pr
22
use ic_certification::Label;
33
use ic_transport_types::Envelope;
44
use ic_transport_types::EnvelopeContent::ReadState;
5+
use pocket_ic::common::rest::{BlockmakerConfigs, RawSubnetBlockmaker, TickConfigs};
56
use pocket_ic::management_canister::{
67
CanisterIdRecord, CanisterInstallMode, CanisterSettings, EcdsaPublicKeyResult,
7-
HttpRequestResult, ProvisionalCreateCanisterWithCyclesArgs, SchnorrAlgorithm,
8-
SchnorrPublicKeyArgsKeyId, SchnorrPublicKeyResult, SignWithBip341Aux, SignWithSchnorrAux,
8+
HttpRequestResult, NodeMetricsHistoryArgs, NodeMetricsHistoryResultItem,
9+
ProvisionalCreateCanisterWithCyclesArgs, SchnorrAlgorithm, SchnorrPublicKeyArgsKeyId,
10+
SchnorrPublicKeyResult, SignWithBip341Aux, SignWithSchnorrAux,
911
};
1012
use pocket_ic::{
1113
common::rest::{
@@ -2214,3 +2216,82 @@ fn test_reject_response_type() {
22142216
assert!(!err.certified);
22152217
}
22162218
}
2219+
2220+
#[test]
2221+
fn test_custom_blockmaker_metrics() {
2222+
const HOURS_IN_SECONDS: u64 = 60 * 60;
2223+
2224+
let pocket_ic = PocketIcBuilder::new().with_application_subnet().build();
2225+
let topology = pocket_ic.topology();
2226+
let application_subnet = topology.get_app_subnets()[0];
2227+
2228+
// Create and install test canister.
2229+
let canister = pocket_ic.create_canister_on_subnet(None, None, application_subnet);
2230+
pocket_ic.add_cycles(canister, INIT_CYCLES);
2231+
pocket_ic.install_canister(canister, test_canister_wasm(), vec![], None);
2232+
2233+
let nodes = topology
2234+
.subnet_configs
2235+
.get(&application_subnet)
2236+
.unwrap()
2237+
.clone();
2238+
2239+
let blockmaker_1 = nodes.node_ids[0].clone();
2240+
let blockmaker_2 = nodes.node_ids[1].clone();
2241+
2242+
let subnets_blockmakers = vec![RawSubnetBlockmaker {
2243+
subnet: application_subnet.into(),
2244+
blockmaker: blockmaker_1.clone(),
2245+
failed_blockmakers: vec![blockmaker_2.clone()],
2246+
}];
2247+
2248+
let tick_configs = TickConfigs {
2249+
blockmakers: Some(BlockmakerConfigs {
2250+
blockmakers_per_subnet: subnets_blockmakers,
2251+
}),
2252+
};
2253+
let daily_blocks = 5;
2254+
2255+
// Blockmaker metrics are recorded in the management canister
2256+
for _ in 0..daily_blocks {
2257+
pocket_ic.tick_with_configs(tick_configs.clone());
2258+
}
2259+
// Advance time until next day so that management canister can record blockmaker metrics
2260+
pocket_ic.advance_time(std::time::Duration::from_secs(HOURS_IN_SECONDS * 24));
2261+
pocket_ic.tick();
2262+
2263+
let response = pocket_ic
2264+
.update_call(
2265+
canister,
2266+
Principal::anonymous(),
2267+
// Calls the node_metrics_history method on the management canister
2268+
"node_metrics_history_proxy",
2269+
Encode!(&NodeMetricsHistoryArgs {
2270+
subnet_id: application_subnet,
2271+
start_at_timestamp_nanos: 0,
2272+
})
2273+
.unwrap(),
2274+
)
2275+
.unwrap();
2276+
2277+
let first_node_metrics = Decode!(&response, Vec<NodeMetricsHistoryResultItem>)
2278+
.unwrap()
2279+
.remove(0)
2280+
.node_metrics;
2281+
2282+
let blockmaker_1_metrics = first_node_metrics
2283+
.iter()
2284+
.find(|x| x.node_id == Principal::from(blockmaker_1.clone()))
2285+
.unwrap()
2286+
.clone();
2287+
let blockmaker_2_metrics = first_node_metrics
2288+
.into_iter()
2289+
.find(|x| x.node_id == Principal::from(blockmaker_2.clone()))
2290+
.unwrap();
2291+
2292+
assert_eq!(blockmaker_1_metrics.num_blocks_proposed_total, daily_blocks);
2293+
assert_eq!(blockmaker_1_metrics.num_block_failures_total, 0);
2294+
2295+
assert_eq!(blockmaker_2_metrics.num_blocks_proposed_total, 0);
2296+
assert_eq!(blockmaker_2_metrics.num_block_failures_total, daily_blocks);
2297+
}

rs/pocket_ic_server/src/lib.rs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,10 @@ pub mod pocket_ic;
3838
pub mod state_api;
3939

4040
use crate::state_api::state::OpOut;
41-
use ::pocket_ic::common::rest::{BinaryBlob, BlobId};
41+
use ::pocket_ic::common::rest::{BinaryBlob, BlobId, RawSubnetBlockmaker};
4242
use axum::async_trait;
43+
use candid::Principal;
44+
use ic_types::{NodeId, PrincipalId, SubnetId};
4345
use pocket_ic::PocketIc;
4446
use serde::Deserialize;
4547

@@ -91,3 +93,28 @@ pub fn copy_dir(
9193
}
9294
Ok(())
9395
}
96+
97+
#[derive(Clone, Debug)]
98+
pub struct SubnetBlockmaker {
99+
pub subnet: SubnetId,
100+
pub blockmaker: NodeId,
101+
pub failed_blockmakers: Vec<NodeId>,
102+
}
103+
104+
impl From<RawSubnetBlockmaker> for SubnetBlockmaker {
105+
fn from(raw: RawSubnetBlockmaker) -> Self {
106+
let subnet = SubnetId::from(PrincipalId::from(Principal::from(raw.subnet)));
107+
let blockmaker = NodeId::from(PrincipalId::from(Principal::from(raw.blockmaker)));
108+
let failed_blockmakers: Vec<NodeId> = raw
109+
.failed_blockmakers
110+
.into_iter()
111+
.map(|node_id| NodeId::from(PrincipalId::from(Principal::from(node_id))))
112+
.collect();
113+
114+
SubnetBlockmaker {
115+
subnet,
116+
blockmaker,
117+
failed_blockmakers,
118+
}
119+
}
120+
}

rs/pocket_ic_server/src/pocket_ic.rs

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::state_api::state::{HasStateLabel, OpOut, PocketIcError, StateLabel};
2-
use crate::{async_trait, copy_dir, BlobStore, OpId, Operation};
2+
use crate::{async_trait, copy_dir, BlobStore, OpId, Operation, SubnetBlockmaker};
33
use askama::Template;
44
use axum::{
55
extract::State,
@@ -54,6 +54,7 @@ use ic_state_machine_tests::{
5454
SubmitIngressError, Subnets,
5555
};
5656
use ic_test_utilities_registry::add_subnet_list_record;
57+
use ic_types::batch::BlockmakerMetrics;
5758
use ic_types::ingress::{IngressState, IngressStatus};
5859
use ic_types::{
5960
artifact::UnvalidatedArtifactMutation,
@@ -77,7 +78,7 @@ use pocket_ic::common::rest::{
7778
self, BinaryBlob, BlobCompression, CanisterHttpHeader, CanisterHttpMethod, CanisterHttpRequest,
7879
CanisterHttpResponse, ExtendedSubnetConfigSet, MockCanisterHttpResponse, RawAddCycles,
7980
RawCanisterCall, RawCanisterId, RawEffectivePrincipal, RawMessageId, RawSetStableMemory,
80-
SubnetInstructionConfig, SubnetKind, SubnetSpec, Topology,
81+
SubnetInstructionConfig, SubnetKind, SubnetSpec, TickConfigs, Topology,
8182
};
8283
use pocket_ic::{ErrorCode, RejectCode, RejectResponse};
8384
use serde::{Deserialize, Serialize};
@@ -1579,14 +1580,82 @@ impl Operation for PubKey {
15791580
}
15801581
}
15811582

1582-
#[derive(Copy, Clone, Debug)]
1583-
pub struct Tick;
1583+
#[derive(Clone, Debug)]
1584+
pub struct Tick {
1585+
pub configs: TickConfigs,
1586+
}
1587+
1588+
impl Tick {
1589+
fn validate_blockmakers_per_subnet(
1590+
&self,
1591+
pic: &mut PocketIc,
1592+
subnets_blockmaker: &[SubnetBlockmaker],
1593+
) -> Result<(), OpOut> {
1594+
for subnet_blockmaker in subnets_blockmaker {
1595+
if subnet_blockmaker
1596+
.failed_blockmakers
1597+
.contains(&subnet_blockmaker.blockmaker)
1598+
{
1599+
return Err(OpOut::Error(PocketIcError::BlockmakerContainedInFailed(
1600+
subnet_blockmaker.blockmaker,
1601+
)));
1602+
}
1603+
1604+
let Some(state_machine) = pic.get_subnet_with_id(subnet_blockmaker.subnet) else {
1605+
return Err(OpOut::Error(PocketIcError::SubnetNotFound(
1606+
subnet_blockmaker.subnet.get().0,
1607+
)));
1608+
};
1609+
1610+
let mut request_blockmakers = subnet_blockmaker.failed_blockmakers.clone();
1611+
request_blockmakers.push(subnet_blockmaker.blockmaker);
1612+
let subnet_nodes: Vec<_> = state_machine.nodes.iter().map(|n| n.node_id).collect();
1613+
for blockmaker in request_blockmakers {
1614+
if !subnet_nodes.contains(&blockmaker) {
1615+
return Err(OpOut::Error(PocketIcError::BlockmakerNotFound(blockmaker)));
1616+
}
1617+
}
1618+
}
1619+
Ok(())
1620+
}
1621+
}
15841622

15851623
impl Operation for Tick {
15861624
fn compute(&self, pic: &mut PocketIc) -> OpOut {
1587-
for subnet in pic.subnets.get_all() {
1588-
subnet.state_machine.execute_round();
1625+
let blockmakers_per_subnet = self.configs.blockmakers.as_ref().map(|cfg| {
1626+
cfg.blockmakers_per_subnet
1627+
.iter()
1628+
.cloned()
1629+
.map(SubnetBlockmaker::from)
1630+
.collect_vec()
1631+
});
1632+
1633+
if let Some(ref bm_per_subnet) = blockmakers_per_subnet {
1634+
if let Err(error) = self.validate_blockmakers_per_subnet(pic, bm_per_subnet) {
1635+
return error;
1636+
}
15891637
}
1638+
1639+
let subnets = pic.subnets.subnets.read().unwrap();
1640+
for (subnet_id, subnet) in subnets.iter() {
1641+
let blockmaker_metrics = blockmakers_per_subnet.as_ref().and_then(|bm_per_subnet| {
1642+
bm_per_subnet
1643+
.iter()
1644+
.find(|bm| bm.subnet == *subnet_id)
1645+
.map(|bm| BlockmakerMetrics {
1646+
blockmaker: bm.blockmaker,
1647+
failed_blockmakers: bm.failed_blockmakers.clone(),
1648+
})
1649+
});
1650+
1651+
match blockmaker_metrics {
1652+
Some(metrics) => subnet
1653+
.state_machine
1654+
.execute_round_with_blockmaker_metrics(metrics),
1655+
None => subnet.state_machine.execute_round(),
1656+
}
1657+
}
1658+
15901659
OpOut::NoOutput
15911660
}
15921661

0 commit comments

Comments
 (0)