Skip to content

Commit f4fca79

Browse files
committed
mc: add support for storm timeouts and mem limits
1 parent 1b1732f commit f4fca79

File tree

5 files changed

+137
-34
lines changed

5 files changed

+137
-34
lines changed

src/main.rs

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use std::{
1111
path::PathBuf,
1212
process::ExitCode,
1313
sync::{Arc, Mutex},
14+
time::{Duration, Instant},
1415
};
1516

1617
use crate::{
@@ -29,7 +30,7 @@ use intrinsic::{annotations::init_calculi, distributions::init_distributions, li
2930
use mc::run_storm::{run_storm, storm_result_to_diagnostic};
3031
use proof_rules::init_encodings;
3132
use regex::Regex;
32-
use resource_limits::{await_with_resource_limits, LimitError, LimitsRef};
33+
use resource_limits::{await_with_resource_limits, LimitError, LimitsRef, MemorySize};
3334
use servers::{run_lsp_server, CliServer, LspServer, Server, ServerError};
3435
use slicing::init_slicing;
3536
use thiserror::Error;
@@ -183,7 +184,17 @@ pub struct ResourceLimitOptions {
183184

184185
/// Memory usage limit in megabytes.
185186
#[arg(long = "mem", default_value = "8192")]
186-
pub mem_limit: u64,
187+
pub mem_limit: usize,
188+
}
189+
190+
impl ResourceLimitOptions {
191+
fn timeout(&self) -> Duration {
192+
Duration::from_secs(self.timeout)
193+
}
194+
195+
fn mem_limit(&self) -> MemorySize {
196+
MemorySize::megabytes(self.mem_limit)
197+
}
187198
}
188199

189200
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
@@ -239,6 +250,19 @@ pub struct JaniOptions {
239250
/// for constants in the model.
240251
#[arg(long)]
241252
pub storm_constants: Option<String>,
253+
254+
/// Timeout in seconds for running Storm.
255+
///
256+
/// Caesar uses the minimum of this value and the remaining time from the
257+
/// `--timeout` option.
258+
#[arg(long)]
259+
pub storm_timeout: Option<u64>,
260+
}
261+
262+
impl JaniOptions {
263+
pub fn storm_timeout(&self) -> Option<Duration> {
264+
self.storm_timeout.map(Duration::from_secs)
265+
}
242266
}
243267

244268
#[derive(Debug, Default, Args)]
@@ -454,7 +478,7 @@ fn finalize_verify_result(
454478
rlimit_options: &ResourceLimitOptions,
455479
verify_result: Result<bool, VerifyError>,
456480
) -> ExitCode {
457-
let (timeout, mem_limit) = (rlimit_options.timeout, rlimit_options.mem_limit);
481+
let (timeout, mem_limit) = (rlimit_options.timeout(), rlimit_options.mem_limit());
458482
match verify_result {
459483
#[allow(clippy::bool_to_int_with_if)]
460484
Ok(all_verified) => ExitCode::from(if all_verified { 0 } else { 1 }),
@@ -467,11 +491,14 @@ fn finalize_verify_result(
467491
ExitCode::from(1)
468492
}
469493
Err(VerifyError::LimitError(LimitError::Timeout)) => {
470-
tracing::error!("Timed out after {} seconds, exiting.", timeout);
494+
tracing::error!("Timed out after {} seconds, exiting.", timeout.as_secs());
471495
std::process::exit(2); // exit ASAP
472496
}
473497
Err(VerifyError::LimitError(LimitError::Oom)) => {
474-
tracing::error!("Exhausted {} megabytes of memory, exiting.", mem_limit);
498+
tracing::error!(
499+
"Exhausted {} megabytes of memory, exiting.",
500+
mem_limit.as_megabytes()
501+
);
475502
std::process::exit(3); // exit ASAP
476503
}
477504
Err(VerifyError::UserError(err)) => {
@@ -587,8 +614,8 @@ pub async fn verify_files(
587614
};
588615
// Unpacking lots of Results with `.await??` :-)
589616
await_with_resource_limits(
590-
Some(options.rlimit_options.timeout),
591-
Some(options.rlimit_options.mem_limit),
617+
Some(options.rlimit_options.timeout()),
618+
Some(options.rlimit_options.mem_limit()),
592619
handle,
593620
)
594621
.await??
@@ -671,7 +698,7 @@ pub(crate) fn verify_test(source: &str) -> (Result<bool, VerifyError>, servers::
671698
.id;
672699

673700
let options = Arc::new(options);
674-
let limits_ref = LimitsRef::new(None);
701+
let limits_ref = LimitsRef::new(None, None);
675702
let res = verify_files_main(&options, limits_ref, &mut server, &[file_id]);
676703
(res, server)
677704
}
@@ -756,6 +783,7 @@ fn verify_files_main(
756783
&options.jani_options,
757784
&mut source_units,
758785
server,
786+
&limits_ref,
759787
&tcx,
760788
false,
761789
)?;
@@ -942,10 +970,14 @@ fn to_jani_loader(
942970
&mut *server_lock,
943971
&user_files,
944972
)?;
973+
let timeout = Instant::now() + options.rlimit_options.timeout();
974+
let mem_limit = options.rlimit_options.mem_limit();
975+
let limits_ref = LimitsRef::new(Some(timeout), Some(mem_limit));
945976
to_jani_main(
946977
&options.jani_options,
947978
&mut source_units,
948979
server_lock.deref_mut(),
980+
&limits_ref,
949981
&tcx,
950982
true,
951983
)
@@ -955,6 +987,7 @@ fn to_jani_main(
955987
options: &JaniOptions,
956988
source_units: &mut Vec<Item<SourceUnit>>,
957989
server: &mut dyn Server,
990+
limits_ref: &LimitsRef,
958991
tcx: &TyCtx,
959992
is_jani_command: bool,
960993
) -> Result<(), VerifyError> {
@@ -985,8 +1018,8 @@ fn to_jani_main(
9851018
Err(err) => Err(err)?,
9861019
Ok(Some(path)) => {
9871020
tracing::debug!(file=?path.display(), "wrote JANI file");
988-
if let Some(which_storm) = options.run_storm {
989-
let res = run_storm(&options, which_storm, &path, vec!["reward".to_owned()]);
1021+
if options.run_storm.is_some() {
1022+
let res = run_storm(&options, &path, vec!["reward".to_owned()], limits_ref);
9901023
server.add_diagnostic(storm_result_to_diagnostic(
9911024
&res,
9921025
source_unit.diagnostic_span(),

src/mc/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ pub fn proc_to_model(
216216

217217
fn check_calculus_annotation(proc: &ProcDecl) -> Result<(), JaniConversionError> {
218218
if let Some(calculus) = proc.calculus {
219-
if &calculus.name != "wp" || &calculus.name != "ert"
219+
if &calculus.name != "wp" && &calculus.name != "ert"
220220
// yeah that's ugly
221221
{
222222
return Err(JaniConversionError::UnsupportedCalculus {

src/mc/run_storm.rs

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,20 @@ use std::{
44
io,
55
path::Path,
66
process::{Command, ExitStatus},
7+
time::Duration,
78
};
89

910
use ariadne::ReportKind;
1011
use indexmap::IndexMap;
1112
use lsp_types::NumberOrString;
1213
use num::BigRational;
14+
use regex::Regex;
1315
use thiserror::Error;
1416
use z3rro::util::PrettyRational;
1517

1618
use crate::{
1719
ast::{Diagnostic, Label, Span},
20+
resource_limits::LimitsRef,
1821
JaniOptions, RunWhichStorm,
1922
};
2023

@@ -62,23 +65,35 @@ pub enum StormError {
6265
Io(#[from] io::Error),
6366
#[error("{0}: {1}")]
6467
StormFailed(ExitStatus, String),
68+
#[error("State exploration aborted after {} seconds, explored {} states", .0.as_secs(), .1)]
69+
StateExplorationAborted(Duration, usize),
6570
#[error("Docker is not running")]
6671
DockerNotRunning,
6772
}
6873

6974
/// Run Storm on the given JANI file and look for output for the given properties.
75+
///
76+
/// Panics if `options.run_storm` is `None`.
7077
pub fn run_storm(
7178
options: &JaniOptions,
72-
option: RunWhichStorm,
7379
mut jani_file: &Path,
7480
properties: Vec<String>,
81+
limits_ref: &LimitsRef,
7582
) -> Result<StormOutput, StormError> {
76-
let mut command = match option {
77-
RunWhichStorm::Path => Command::new("storm"),
83+
let which_storm = options.run_storm.unwrap();
84+
let mut command = match which_storm {
85+
RunWhichStorm::Path => {
86+
if limits_ref.memory_limit().is_some() {
87+
tracing::debug!("Memory limit is ignored when running Storm from path");
88+
}
89+
90+
Command::new("storm")
91+
}
7892
RunWhichStorm::DockerStable | RunWhichStorm::DockerCI => {
7993
let mut command = Command::new("docker");
8094
let jani_file_dir = jani_file.parent().unwrap().canonicalize().unwrap();
81-
let image_name = match option {
95+
jani_file = jani_file.file_name().unwrap().as_ref();
96+
let image_name = match which_storm {
8297
RunWhichStorm::DockerStable => "movesrwth/storm:stable",
8398
RunWhichStorm::DockerCI => "movesrwth/storm:ci",
8499
_ => unreachable!(),
@@ -89,14 +104,29 @@ pub fn run_storm(
89104
.arg("-v")
90105
.arg(format!("{}:/mnt", jani_file_dir.display()))
91106
.arg("-w")
92-
.arg("/mnt")
93-
.arg(image_name)
94-
.arg("storm");
95-
jani_file = jani_file.file_name().unwrap().as_ref();
107+
.arg("/mnt");
108+
109+
if let Some(mem_limit) = limits_ref.memory_limit() {
110+
command
111+
.arg("--memory")
112+
.arg(format!("{}m", mem_limit.as_megabytes()));
113+
}
114+
115+
command.arg(image_name).arg("storm");
96116
command
97117
}
98118
};
99119

120+
let caesar_timeout = limits_ref.time_left();
121+
let storm_specific_timeout = options.storm_timeout();
122+
let timeout = caesar_timeout
123+
.and_then(|ct| storm_specific_timeout.map(|st| ct.min(st)))
124+
.or(caesar_timeout)
125+
.or(storm_specific_timeout);
126+
if let Some(timeout) = timeout {
127+
command.arg("--timeout").arg(timeout.as_secs().to_string());
128+
}
129+
100130
if options.storm_exact {
101131
command.arg("--exact");
102132
}
@@ -125,7 +155,7 @@ pub fn run_storm(
125155
if !output.status.success() {
126156
if output.status.code() == Some(125)
127157
&& matches!(
128-
option,
158+
which_storm,
129159
RunWhichStorm::DockerStable | RunWhichStorm::DockerCI
130160
)
131161
{
@@ -142,6 +172,17 @@ pub fn run_storm(
142172
.collect(),
143173
});
144174
}
175+
176+
let re = Regex::new(r"Explored (\d+) states in (\d+) seconds before abort").unwrap();
177+
if let Some(captures) = re.captures(&output_str) {
178+
let states_explored: usize = captures[1].parse().unwrap();
179+
let time_taken: u64 = captures[2].parse().unwrap();
180+
return Err(StormError::StateExplorationAborted(
181+
Duration::from_secs(time_taken),
182+
states_explored,
183+
));
184+
}
185+
145186
Err(StormError::StormFailed(output.status, output_str))
146187
} else {
147188
let results = parse_property_results(options, properties, &output_str);

0 commit comments

Comments
 (0)