Skip to content

Commit 1bee46b

Browse files
committed
Merge branch 'ulan/run-594' into 'master'
RUN-594: Introduce an explicit limit for number of slices in DTS This makes DTS more robust in cases when slices do not use all the available instructions. See merge request dfinity-lab/public/ic!11565
2 parents 93d3180 + bffceec commit 1bee46b

File tree

2 files changed

+72
-3
lines changed

2 files changed

+72
-3
lines changed

rs/canister_sandbox/backend_lib/src/dts.rs

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@ use ic_interfaces::execution_environment::{
55
use ic_types::NumInstructions;
66
use std::sync::{Arc, Condvar, Mutex};
77

8+
// The upper bound on the number of supported execution slices.
9+
//
10+
// It is set to 400 because 500 the number of rounds in an epoch,
11+
// which the interval between two checkpoints.
12+
//
13+
// Note that currently the maximum number of slices:
14+
// - for update messages is 10.
15+
// - for upgrade messages is 100.
16+
// See constants in `rs/config/src/subnet_config.rs`
17+
const MAX_NUM_SLICES: i64 = 400;
18+
819
// Indicates the current state of execution.
920
// It start with `Running` and may transition to `Paused`.
1021
// From `Paused` it transitions either to `Running` or `Aborted`.
@@ -34,19 +45,37 @@ struct State {
3445
// Invariant: it does not exceed `total_instruction_limit`.
3546
instructions_executed: i64,
3647

48+
// The number of remaining execution slices.
49+
//
50+
// Note that normally execution finishes when `instructions_executed`
51+
// exceeds `total_instruction_limit`. This counter is used as an additional
52+
// safeguard to guarantee fast progress for the case when each slice does
53+
// not use all its available instructions.
54+
slices_left: i64,
55+
3756
// The execution complexity accumulated at the beginning of the round.
3857
execution_complexity: ExecutionComplexity,
3958
}
4059

4160
impl State {
4261
fn new(total_instruction_limit: i64, max_slice_instruction_limit: i64) -> Self {
4362
let max_slice_instruction_limit = max_slice_instruction_limit.min(total_instruction_limit);
63+
64+
let max_num_slices =
65+
(total_instruction_limit / max_slice_instruction_limit.max(1)).min(MAX_NUM_SLICES);
66+
67+
// Since the number of slices is a secondary limit and just a safeguard
68+
// in addition to the primary limit of instructions, we can give it some
69+
// slack to ensure that it doesn't interfere in regular cases.
70+
let max_num_slices_with_slack = (2 * max_num_slices).clamp(4, MAX_NUM_SLICES);
71+
4472
let result = Self {
4573
execution_status: ExecutionStatus::Running,
4674
total_instruction_limit,
4775
max_slice_instruction_limit,
4876
slice_instruction_limit: max_slice_instruction_limit,
4977
instructions_executed: 0,
78+
slices_left: max_num_slices_with_slack,
5079
execution_complexity: ExecutionComplexity::default(),
5180
};
5281
result.check_invariants();
@@ -73,7 +102,7 @@ impl State {
73102
/// Returns true if the current slice is sufficient to reach the total
74103
/// instruction limit.
75104
fn is_last_slice(&self) -> bool {
76-
self.slice_instruction_limit >= self.total_instructions_left()
105+
self.slice_instruction_limit >= self.total_instructions_left() || self.slices_left <= 1
77106
}
78107

79108
/// Computes the limit for the next slice taking into account
@@ -83,8 +112,7 @@ impl State {
83112
let newly_executed = self.newly_executed(instruction_counter);
84113
let carry_over = (newly_executed - self.slice_instruction_limit).max(0);
85114
(self.max_slice_instruction_limit - carry_over)
86-
.min(self.total_instructions_left())
87-
.max(0)
115+
.clamp(0, self.total_instructions_left().max(0))
88116
}
89117

90118
/// Returns the number of instructions executed in the current slice.
@@ -113,6 +141,7 @@ impl State {
113141
.saturating_add(self.newly_executed(instruction_counter));
114142
self.slice_instruction_limit = self.next_slice_instruction_limit(instruction_counter);
115143
self.execution_complexity = execution_complexity;
144+
self.slices_left -= 1;
116145
self.check_invariants();
117146
}
118147

rs/canister_sandbox/backend_lib/src/dts/tests.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ use ic_interfaces::execution_environment::{
88
};
99
use ic_types::NumInstructions;
1010

11+
use crate::dts::MAX_NUM_SLICES;
12+
1113
use super::{DeterministicTimeSlicingHandler, PausedExecution};
1214

1315
#[test]
@@ -62,6 +64,17 @@ fn dts_state_updates_saturating() {
6264
assert!(state.is_last_slice());
6365
}
6466

67+
#[test]
68+
fn dts_max_num_slices() {
69+
let mut state = super::State::new(1000000, 100);
70+
let mut iterations = 0;
71+
while !state.is_last_slice() {
72+
state.update(99, ExecutionComplexity::default());
73+
iterations += 1;
74+
}
75+
assert!(iterations <= MAX_NUM_SLICES);
76+
}
77+
6578
#[test]
6679
fn pause_and_resume_works() {
6780
let (tx, rx): (Sender<PausedExecution>, Receiver<PausedExecution>) = mpsc::channel();
@@ -149,3 +162,30 @@ fn invalid_instructions() {
149162
drop(dts);
150163
control_thread.join().unwrap();
151164
}
165+
166+
#[test]
167+
fn max_num_slices() {
168+
let (tx, rx): (Sender<PausedExecution>, Receiver<PausedExecution>) = mpsc::channel();
169+
let dts = DeterministicTimeSlicingHandler::new(1000000, 100, move |_slice, paused| {
170+
tx.send(paused).unwrap();
171+
});
172+
let control_thread = thread::spawn(move || {
173+
for _ in 0..MAX_NUM_SLICES - 1 {
174+
let paused_execution = rx.recv().unwrap();
175+
paused_execution.resume();
176+
}
177+
});
178+
179+
let mut result = Ok(0);
180+
for i in 0..MAX_NUM_SLICES {
181+
result = dts.out_of_instructions(99, Default::default());
182+
if i < MAX_NUM_SLICES - 1 {
183+
assert!(result.is_ok());
184+
} else {
185+
assert!(result.is_err());
186+
}
187+
}
188+
assert_eq!(result, Err(HypervisorError::InstructionLimitExceeded));
189+
drop(dts);
190+
control_thread.join().unwrap();
191+
}

0 commit comments

Comments
 (0)