Skip to content

Conversation

adamspofford-dfinity
Copy link
Contributor

No description provided.

@adamspofford-dfinity adamspofford-dfinity changed the base branch from spofford/async-timers to main July 24, 2025 13:15
@adamspofford-dfinity adamspofford-dfinity changed the base branch from main to spofford/async-timers July 24, 2025 13:15
Base automatically changed from spofford/async-timers to main August 4, 2025 16:14
@adamspofford-dfinity adamspofford-dfinity marked this pull request as ready for review August 4, 2025 19:11
@adamspofford-dfinity adamspofford-dfinity requested a review from a team as a code owner August 4, 2025 19:11
Copy link
Contributor

@lwshang lwshang left a comment

Choose a reason for hiding this comment

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

LGTM. Ready for the security review.

I left a few minor suggestions for cleanup.

TIMERS.with_borrow_mut(|timers| {
let mut to_reschedule = Vec::new();
// pop every timer that should have been completed by `now`, and get ready to run its task if it exists
loop {
Copy link

Choose a reason for hiding this comment

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

The number of iterations seems to be only bounded by the number of timers (so we could run out of cycles during looping). I would prefer if we define a reasonable maximum amount of timers we process per invocation.

Copy link
Contributor Author

@adamspofford-dfinity adamspofford-dfinity Sep 30, 2025

Choose a reason for hiding this comment

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

What would be a reasonable maximum? The system will already start interrupting it for us when we hit 500. And is the cycles cost of the loop continuing meaningful in such a case? We already check cost_call - would it be enough to just check that we've got at least 2x that remaining (since I assume the cost of the call would outweigh the rest of the code)?

Copy link

Choose a reason for hiding this comment

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

Sorry what I really meant with "run out of cycles during looping" is that we hit the instruction limit of global_timer() invocation (40B). We should never trap because we scheduled too many inter-canister calls, because if there are not enough cycles to perform a call, call_perform() should just return an error and the cycles needed to complete execution of global_timer() should be already reserved before the invocation starts.

One option to define a limit would be that you call A=ic0.performance_counter(0) after executing the first iteration of the loop and stop processing timers when, e.g., ic0.performance_counter(0)+A*2 > 40B. The chosen multiplier X should ensure that A*X exceeds the worst case instruction costs of a loop iteration.

ic0::debug_print(
b"[ic-cdk-timers] unable to schedule timer: not enough liquid cycles",
);
return;
Copy link

@tmu0 tmu0 Sep 8, 2025

Choose a reason for hiding this comment

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

I think in this case we need to

  1. put the timer in to_reschedule
  2. break the loop instead of returning in order to execute the code after the loop

2 => {
// Try to execute the timer again later.
TIMERS.with_borrow_mut(|timers| timers.push(timer));
return;
Copy link

Choose a reason for hiding this comment

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

How is it guaranteed that the timer is ever run again if update_ic0_timer() is not called?

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.

3 participants