Skip to content

Commit 17e42ae

Browse files
authored
feat(PocketIC): new PocketIC operation to set certified time (dfinity#3595)
This PR introduces a new PocketIC operation to set certified time so that query calls and read state requests are evaluated on a state with that (certified) time. Using this new operation to set time in the "live" mode also fixes test [flakiness](https://github.com/dfinity/ic/actions/runs/12909846684/job/35998504336).
1 parent 52f75b8 commit 17e42ae

File tree

8 files changed

+142
-56
lines changed

8 files changed

+142
-56
lines changed

packages/pocket-ic/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
If the status of the update call is known, but the update call was submitted by a different caller, then an error is returned.
1616
- The function `PocketIc::await_call_no_ticks` to await the status of an update call (submitted through an ingress message) becoming known without triggering round execution
1717
(round execution must be triggered separarely, e.g., on a "live" instance or by separate PocketIC library calls).
18+
- The function `PocketIc::set_certified_time` to set the current certified time on all subnets of the PocketIC instance.
1819

1920
### Changed
2021
- The response types `pocket_ic::WasmResult`, `pocket_ic::UserError`, and `pocket_ic::CallError` are replaced by a single reject response type `pocket_ic::RejectResponse`.

packages/pocket-ic/src/lib.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -619,6 +619,13 @@ impl PocketIc {
619619
runtime.block_on(async { self.pocket_ic.set_time(time).await })
620620
}
621621

622+
/// Set the current certified time of the IC, on all subnets.
623+
#[instrument(skip(self), fields(instance_id=self.pocket_ic.instance_id, time = ?time))]
624+
pub fn set_certified_time(&self, time: SystemTime) {
625+
let runtime = self.runtime.clone();
626+
runtime.block_on(async { self.pocket_ic.set_certified_time(time).await })
627+
}
628+
622629
/// Advance the time on the IC on all subnets by some nanoseconds.
623630
#[instrument(skip(self), fields(instance_id=self.pocket_ic.instance_id, duration = ?duration))]
624631
pub fn advance_time(&self, duration: Duration) {

packages/pocket-ic/src/nonblocking.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ impl PocketIc {
302302
#[instrument(skip(self), fields(instance_id=self.instance_id))]
303303
pub async fn auto_progress(&self) -> Url {
304304
let now = std::time::SystemTime::now();
305-
self.set_time(now).await;
305+
self.set_certified_time(now).await;
306306
let endpoint = "auto_progress";
307307
let auto_progress_config = AutoProgressConfig {
308308
artificial_delay_ms: None,
@@ -485,6 +485,22 @@ impl PocketIc {
485485
.await;
486486
}
487487

488+
/// Set the current certified time of the IC, on all subnets.
489+
#[instrument(skip(self), fields(instance_id=self.instance_id, time = ?time))]
490+
pub async fn set_certified_time(&self, time: SystemTime) {
491+
let endpoint = "update/set_certified_time";
492+
self.post::<(), _>(
493+
endpoint,
494+
RawTime {
495+
nanos_since_epoch: time
496+
.duration_since(SystemTime::UNIX_EPOCH)
497+
.expect("Time went backwards")
498+
.as_nanos() as u64,
499+
},
500+
)
501+
.await;
502+
}
503+
488504
/// Advance the time on the IC on all subnets by some nanoseconds.
489505
#[instrument(skip(self), fields(instance_id=self.instance_id, duration = ?duration))]
490506
pub async fn advance_time(&self, duration: Duration) {

packages/pocket-ic/tests/tests.rs

Lines changed: 42 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -350,40 +350,6 @@ fn test_multiple_large_xnet_payloads() {
350350
}
351351
}
352352

353-
#[test]
354-
fn test_get_and_set_and_advance_time() {
355-
let pic = PocketIc::new();
356-
357-
let unix_time_secs = 1630328630;
358-
let set_time = SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(unix_time_secs);
359-
pic.set_time(set_time);
360-
assert_eq!(pic.get_time(), set_time);
361-
pic.tick();
362-
assert_eq!(pic.get_time(), set_time);
363-
364-
pic.advance_time(std::time::Duration::from_secs(420));
365-
assert_eq!(
366-
pic.get_time(),
367-
set_time + std::time::Duration::from_secs(420)
368-
);
369-
pic.tick();
370-
assert_eq!(
371-
pic.get_time(),
372-
set_time + std::time::Duration::from_secs(420)
373-
);
374-
}
375-
376-
#[test]
377-
#[should_panic(expected = "SettingTimeIntoPast")]
378-
fn set_time_into_past() {
379-
let pic = PocketIc::new();
380-
381-
let now = SystemTime::now();
382-
pic.set_time(now + std::time::Duration::from_secs(1));
383-
384-
pic.set_time(now);
385-
}
386-
387353
fn query_and_check_time(pic: &PocketIc, test_canister: Principal) {
388354
let current_time = pic
389355
.get_time()
@@ -402,25 +368,62 @@ fn query_and_check_time(pic: &PocketIc, test_canister: Principal) {
402368
}
403369

404370
#[test]
405-
fn query_call_after_advance_time() {
371+
fn test_get_and_set_and_advance_time() {
406372
let pic = PocketIc::new();
407373

408374
// We create a test canister.
409375
let canister = pic.create_canister();
410376
pic.add_cycles(canister, INIT_CYCLES);
411377
pic.install_canister(canister, test_canister_wasm(), vec![], None);
412378

379+
let unix_time_secs = 1650000000;
380+
let time = SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(unix_time_secs);
381+
pic.set_time(time);
382+
// time is not certified so `query_and_check_time` would fail here
383+
assert_eq!(pic.get_time(), time);
384+
pic.tick();
413385
query_and_check_time(&pic, canister);
414-
415-
pic.advance_time(std::time::Duration::from_secs(420));
386+
assert_eq!(pic.get_time(), time);
416387
pic.tick();
417-
418388
query_and_check_time(&pic, canister);
389+
assert_eq!(pic.get_time(), time + std::time::Duration::from_nanos(1));
419390

420-
pic.advance_time(std::time::Duration::from_secs(0));
391+
let unix_time_secs = 1700000000;
392+
let time = SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(unix_time_secs);
393+
pic.set_certified_time(time);
394+
query_and_check_time(&pic, canister);
395+
assert_eq!(pic.get_time(), time);
396+
pic.tick();
397+
query_and_check_time(&pic, canister);
398+
assert_eq!(pic.get_time(), time + std::time::Duration::from_nanos(1));
421399
pic.tick();
400+
query_and_check_time(&pic, canister);
401+
assert_eq!(pic.get_time(), time + std::time::Duration::from_nanos(2));
422402

403+
let time = pic.get_time();
404+
pic.advance_time(std::time::Duration::from_secs(420));
405+
// time is not certified so `query_and_check_time` would fail here
406+
assert_eq!(pic.get_time(), time + std::time::Duration::from_secs(420));
407+
pic.tick();
423408
query_and_check_time(&pic, canister);
409+
assert_eq!(pic.get_time(), time + std::time::Duration::from_secs(420));
410+
pic.tick();
411+
query_and_check_time(&pic, canister);
412+
assert_eq!(
413+
pic.get_time(),
414+
time + std::time::Duration::from_secs(420) + std::time::Duration::from_nanos(1)
415+
);
416+
}
417+
418+
#[test]
419+
#[should_panic(expected = "SettingTimeIntoPast")]
420+
fn set_time_into_past() {
421+
let pic = PocketIc::new();
422+
423+
let now = SystemTime::now();
424+
pic.set_time(now + std::time::Duration::from_secs(1));
425+
426+
pic.set_time(now);
424427
}
425428

426429
#[test]

rs/pocket_ic_server/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
### Added
1515
- New endpoint `/instances/<instance_id>/read/ingress_status` to fetch the status of an update call submitted through an ingress message.
1616
If an optional caller is provided, the status of the update call is known, but the update call was submitted by a different caller, then an error is returned.
17+
- New endpoint `/instances/<instance_id>/update/set_certified_time` to set the current certified time on all subnets of the PocketIC instance.
1718

1819
### Fixed
1920
- Canisters created via `provisional_create_canister_with_cycles` with the management canister ID as the effective canister ID

rs/pocket_ic_server/src/pocket_ic.rs

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1166,32 +1166,55 @@ pub struct SetTime {
11661166
pub time: Time,
11671167
}
11681168

1169-
impl Operation for SetTime {
1170-
fn compute(&self, pic: &mut PocketIc) -> OpOut {
1171-
// Time is kept in sync across subnets, so one can take any subnet.
1172-
let current_time: SystemTime = pic.any_subnet().time();
1173-
let set_time: SystemTime = self.time.into();
1174-
match current_time.cmp(&set_time) {
1175-
std::cmp::Ordering::Greater => OpOut::Error(PocketIcError::SettingTimeIntoPast((
1176-
systemtime_to_unix_epoch_nanos(current_time),
1177-
systemtime_to_unix_epoch_nanos(set_time),
1178-
))),
1179-
std::cmp::Ordering::Equal => OpOut::NoOutput,
1180-
std::cmp::Ordering::Less => {
1181-
// Sets the time on all subnets.
1182-
for subnet in pic.subnets.get_all() {
1169+
fn set_time(pic: &PocketIc, time: Time, certified: bool) -> OpOut {
1170+
// Time is kept in sync across subnets, so one can take any subnet.
1171+
let current_time: SystemTime = pic.any_subnet().time();
1172+
let set_time: SystemTime = time.into();
1173+
match current_time.cmp(&set_time) {
1174+
std::cmp::Ordering::Greater => OpOut::Error(PocketIcError::SettingTimeIntoPast((
1175+
systemtime_to_unix_epoch_nanos(current_time),
1176+
systemtime_to_unix_epoch_nanos(set_time),
1177+
))),
1178+
std::cmp::Ordering::Equal => OpOut::NoOutput,
1179+
std::cmp::Ordering::Less => {
1180+
// Sets the time on all subnets.
1181+
for subnet in pic.subnets.get_all() {
1182+
if certified {
1183+
subnet.state_machine.set_certified_time(set_time);
1184+
} else {
11831185
subnet.state_machine.set_time(set_time);
11841186
}
1185-
OpOut::NoOutput
11861187
}
1188+
OpOut::NoOutput
11871189
}
11881190
}
1191+
}
1192+
1193+
impl Operation for SetTime {
1194+
fn compute(&self, pic: &mut PocketIc) -> OpOut {
1195+
set_time(pic, self.time, false)
1196+
}
11891197

11901198
fn id(&self) -> OpId {
11911199
OpId(format!("set_time_{}", self.time))
11921200
}
11931201
}
11941202

1203+
#[derive(Clone, Debug)]
1204+
pub struct SetCertifiedTime {
1205+
pub time: Time,
1206+
}
1207+
1208+
impl Operation for SetCertifiedTime {
1209+
fn compute(&self, pic: &mut PocketIc) -> OpOut {
1210+
set_time(pic, self.time, true)
1211+
}
1212+
1213+
fn id(&self) -> OpId {
1214+
OpId(format!("set_certified_time_{}", self.time))
1215+
}
1216+
}
1217+
11951218
#[derive(Copy, Clone, Debug)]
11961219
pub struct GetTopology;
11971220

rs/pocket_ic_server/src/state_api/routes.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use crate::pocket_ic::{
1010
AddCycles, AwaitIngressMessage, CallRequest, CallRequestVersion, CanisterReadStateRequest,
1111
DashboardRequest, GetCanisterHttp, GetControllers, GetCyclesBalance, GetStableMemory,
1212
GetSubnet, GetTime, GetTopology, IngressMessageStatus, MockCanisterHttp, PubKey, Query,
13-
QueryRequest, SetStableMemory, SetTime, StatusRequest, SubmitIngressMessage,
13+
QueryRequest, SetCertifiedTime, SetStableMemory, SetTime, StatusRequest, SubmitIngressMessage,
1414
SubnetReadStateRequest, Tick,
1515
};
1616
use crate::{async_trait, pocket_ic::PocketIc, BlobStore, InstanceId, OpId, Operation};
@@ -99,6 +99,7 @@ where
9999
post(handler_await_ingress_message),
100100
)
101101
.directory_route("/set_time", post(handler_set_time))
102+
.directory_route("/set_certified_time", post(handler_set_certified_time))
102103
.directory_route("/add_cycles", post(handler_add_cycles))
103104
.directory_route("/set_stable_memory", post(handler_set_stable_memory))
104105
.directory_route("/tick", post(handler_tick))
@@ -1035,6 +1036,20 @@ pub async fn handler_set_time(
10351036
(code, Json(response))
10361037
}
10371038

1039+
pub async fn handler_set_certified_time(
1040+
State(AppState { api_state, .. }): State<AppState>,
1041+
Path(instance_id): Path<InstanceId>,
1042+
headers: HeaderMap,
1043+
axum::extract::Json(time): axum::extract::Json<rest::RawTime>,
1044+
) -> (StatusCode, Json<ApiResponse<()>>) {
1045+
let timeout = timeout_or_default(headers);
1046+
let op = SetCertifiedTime {
1047+
time: ic_types::Time::from_nanos_since_unix_epoch(time.nanos_since_epoch),
1048+
};
1049+
let (code, response) = run_operation(api_state, instance_id, timeout, op).await;
1050+
(code, Json(response))
1051+
}
1052+
10381053
pub async fn handler_add_cycles(
10391054
State(AppState { api_state, .. }): State<AppState>,
10401055
Path(instance_id): Path<InstanceId>,

rs/state_machine_tests/src/lib.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2513,6 +2513,26 @@ impl StateMachine {
25132513
.unwrap_or_else(|_| error!(self.replica_logger, "Time went backwards."));
25142514
}
25152515

2516+
/// Certifies the specified time by modifying the time in the replicated state
2517+
/// and certifying that new state.
2518+
pub fn set_certified_time(&self, time: SystemTime) {
2519+
let t = time
2520+
.duration_since(SystemTime::UNIX_EPOCH)
2521+
.unwrap()
2522+
.as_nanos() as u64;
2523+
let time = Time::from_nanos_since_unix_epoch(t);
2524+
let (height, mut replicated_state) = self.state_manager.take_tip();
2525+
replicated_state.metadata.batch_time = time;
2526+
self.state_manager.commit_and_certify(
2527+
replicated_state,
2528+
height.increment(),
2529+
CertificationScope::Metadata,
2530+
None,
2531+
);
2532+
self.set_time(time.into());
2533+
*self.time_of_last_round.write().unwrap() = time;
2534+
}
2535+
25162536
/// Returns the current state machine time.
25172537
/// The time of a round executed by this state machine equals its current time
25182538
/// if its current time increased since the last round.

0 commit comments

Comments
 (0)