Skip to content

Commit 0f3af3b

Browse files
Jeremy Braunfacebook-github-bot
Jeremy Braun
authored andcommitted
let starlark evaluations during analysis be canceled
Summary: Today, starlark evaluations must run to completion. This inserts an "infrequent instruction check" in the evaluator that is called every so often, in which we now check whether the `CancellationContext` has been notified. If so, we return with `EvaluatorError:Cancelled`. #internal This should allow eagerbuild's use of the `--premptible` flag to exit early from starlark evaluations and not hold up the next build in "synchronization and waiting". This also provides a good spot for T219887296 to be addressed Reviewed By: JakobDegen Differential Revision: D73694716 fbshipit-source-id: 718253cf9e37fc8fee3de60f9f3075211f534b19
1 parent 1cb9b1b commit 0f3af3b

File tree

3 files changed

+79
-0
lines changed

3 files changed

+79
-0
lines changed

starlark/src/eval/bc/instr_impl.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1276,6 +1276,9 @@ impl BcInstr for InstrContinue {
12761276
BcAddrOffset,
12771277
),
12781278
) -> InstrControl<'v, 'b> {
1279+
if let Err(e) = eval.run_infrequent_instr_checks() {
1280+
return InstrControl::Err(e);
1281+
}
12791282
let iter = frame.get_bc_slot(*iter);
12801283
let loop_depth = *loop_depth;
12811284
let i = frame.get_iter_index(loop_depth);
@@ -1475,6 +1478,7 @@ impl BcFrozenCallable for FrozenValue {
14751478
args: &Arguments<'v, '_>,
14761479
eval: &mut Evaluator<'v, '_, '_>,
14771480
) -> crate::Result<Value<'v>> {
1481+
eval.run_infrequent_instr_checks()?;
14781482
self.to_value().invoke_with_loc(Some(location), args, eval)
14791483
}
14801484
}
@@ -1487,6 +1491,7 @@ impl BcFrozenCallable for FrozenValueTyped<'static, FrozenDef> {
14871491
args: &Arguments<'v, '_>,
14881492
eval: &mut Evaluator<'v, '_, '_>,
14891493
) -> crate::Result<Value<'v>> {
1494+
eval.run_infrequent_instr_checks()?;
14901495
eval.with_call_stack(self.to_value(), Some(location), |eval| {
14911496
self.as_ref().invoke(self.to_value(), args, eval)
14921497
})
@@ -1501,6 +1506,7 @@ impl BcFrozenCallable for BcNativeFunction {
15011506
args: &Arguments<'v, '_>,
15021507
eval: &mut Evaluator<'v, '_, '_>,
15031508
) -> crate::Result<Value<'v>> {
1509+
eval.run_infrequent_instr_checks()?;
15041510
eval.with_call_stack(self.to_value(), Some(location), |eval| {
15051511
self.invoke(args, eval)
15061512
})
@@ -1545,6 +1551,7 @@ impl<A: BcCallArgs<Symbol>> InstrNoFlowImpl for InstrCallImpl<A> {
15451551
_ip: BcPtrAddr,
15461552
(this, args, span, target): &(BcSlotIn, A, FrozenRef<'static, FrameSpan>, BcSlotOut),
15471553
) -> crate::Result<()> {
1554+
eval.run_infrequent_instr_checks()?;
15481555
let f = frame.get_bc_slot(*this);
15491556
let arguments = Arguments(args.pop_from_stack(frame));
15501557
let r = f.invoke_with_loc(Some(*span), &arguments, eval)?;
@@ -1565,6 +1572,7 @@ impl<F: BcFrozenCallable, A: BcCallArgs<Symbol>> InstrNoFlowImpl
15651572
_ip: BcPtrAddr,
15661573
(fun, args, span, target): &(F, A, FrozenRef<'static, FrameSpan>, BcSlotOut),
15671574
) -> crate::Result<()> {
1575+
eval.run_infrequent_instr_checks()?;
15681576
let arguments = Arguments(args.pop_from_stack(frame));
15691577
let r = fun.bc_invoke(*span, &arguments, eval)?;
15701578
frame.set_bc_slot(*target, r);
@@ -1592,6 +1600,7 @@ impl<A: BcCallArgsForDef> InstrNoFlowImpl for InstrCallFrozenDefImpl<A> {
15921600
BcSlotOut,
15931601
),
15941602
) -> crate::Result<()> {
1603+
eval.run_infrequent_instr_checks()?;
15951604
let arguments = args.pop_from_stack(frame);
15961605
let r = eval.with_call_stack(fun.to_value(), Some(*span), |eval| {
15971606
fun.as_ref()
@@ -1613,6 +1622,7 @@ fn call_method_common<'v>(
16131622
span: FrozenRef<'static, FrameSpan>,
16141623
target: BcSlotOut,
16151624
) -> crate::Result<()> {
1625+
eval.run_infrequent_instr_checks()?;
16161626
// TODO: wrong span: should be span of `object.method`, not of the whole expression
16171627
let method = get_attr_hashed_raw(this, symbol, eval.heap())?;
16181628
let r = method.invoke(this, span, arguments, eval)?;
@@ -1728,6 +1738,7 @@ impl InstrNoFlowImpl for InstrPossibleGcImpl {
17281738
_ip: BcPtrAddr,
17291739
(): &(),
17301740
) -> crate::Result<()> {
1741+
eval.run_infrequent_instr_checks()?;
17311742
possible_gc(eval);
17321743
Ok(())
17331744
}

starlark/src/eval/runtime/evaluator.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,11 +104,16 @@ enum EvaluatorError {
104104
CallstackSizeAlreadySet,
105105
#[error("Max callstack size cannot be zero")]
106106
ZeroCallstackSize,
107+
#[error("Evaluation cancelled")]
108+
Cancelled,
107109
}
108110

109111
/// Number of bytes to allocate between GC's.
110112
pub(crate) const GC_THRESHOLD: usize = 100000;
111113

114+
/// Number of instructions to execute before running "infrequent" checks
115+
const INFREQUENT_INSTRUCTION_CHECK_PERIOD: u32 = 1000;
116+
112117
/// Default value for max starlark stack size
113118
pub(crate) const DEFAULT_STACK_SIZE: usize = 50;
114119

@@ -164,6 +169,10 @@ pub struct Evaluator<'v, 'a, 'e> {
164169
// The Starlark-level call-stack of functions.
165170
// Must go last because it's quite a big structure
166171
pub(crate) call_stack: CheapCallStack<'v>,
172+
/// Function to check if evaluation should be cancelled early
173+
pub(crate) is_cancelled: Box<dyn Fn() -> bool + 'a>,
174+
/// A counter to track when to perform "infrequent" checks like cancellation, timeouts, etc
175+
pub(crate) infrequent_instr_check_counter: u32,
167176
}
168177

169178
/// Just holds things that require using EvaluationCallbacksEnabled so that we can cache whether that needs to be enabled or not.
@@ -234,6 +243,8 @@ impl<'v, 'a, 'e: 'a> Evaluator<'v, 'a, 'e> {
234243
verbose_gc: false,
235244
static_typechecking: false,
236245
max_callstack_size: None,
246+
is_cancelled: Box::new(|| false),
247+
infrequent_instr_check_counter: 0,
237248
}
238249
}
239250

@@ -449,6 +460,11 @@ impl<'v, 'a, 'e: 'a> Evaluator<'v, 'a, 'e> {
449460
self.soft_error_handler = handler;
450461
}
451462

463+
/// Set canceled-checking function. This function is called periodically to check if the evaluator should return early (with an error condition).
464+
pub fn set_check_cancelled(&mut self, is_canceled: Box<dyn Fn() -> bool + 'a>) {
465+
self.is_cancelled = is_canceled
466+
}
467+
452468
/// Called to add an entry to the call stack, by the function being invoked.
453469
/// Called for all types of function, including those written in Rust.
454470
#[inline(always)]
@@ -854,6 +870,20 @@ impl<'v, 'a, 'e: 'a> Evaluator<'v, 'a, 'e> {
854870
self.max_callstack_size = Some(stack_size);
855871
Ok(())
856872
}
873+
874+
#[inline(always)]
875+
pub(crate) fn run_infrequent_instr_checks(&mut self) -> crate::Result<()> {
876+
self.infrequent_instr_check_counter += 1;
877+
if self.infrequent_instr_check_counter >= INFREQUENT_INSTRUCTION_CHECK_PERIOD {
878+
if (self.is_cancelled)() {
879+
return Err(crate::Error::new_other(EvaluatorError::Cancelled));
880+
}
881+
self.infrequent_instr_check_counter = 0
882+
};
883+
884+
// TODO(T219887296): implement CPU-time-limiting checks here
885+
Ok(())
886+
}
857887
}
858888

859889
pub(crate) trait EvaluationCallbacks {

starlark/src/tests/uncategorized.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,44 @@ fn test_module_visibility_preserved_by_evaluator() -> crate::Result<()> {
582582
Ok(())
583583
}
584584

585+
#[test]
586+
fn test_cancellation() -> crate::Result<()> {
587+
// Make sure that when we use a module in the evaluator, the entering / exiting the
588+
// module with ScopeData preserves the visibility of symbols.
589+
590+
let globals = Globals::standard();
591+
let import = Module::new();
592+
593+
let mut eval = Evaluator::new(&import);
594+
eval.set_check_cancelled(Box::new(|| true));
595+
596+
let ast = AstModule::parse(
597+
"prelude.bzl",
598+
// Note that the exact range here is unimportant, so long as it's small enough to not trigger the "infrequent" checks
599+
"def loop():\n for i in range(10):\n pass\nloop()".to_owned(),
600+
&Dialect::Standard,
601+
)
602+
.unwrap();
603+
eval.eval_module(ast, &globals).unwrap();
604+
605+
let ast = AstModule::parse(
606+
"prelude.bzl",
607+
// Note that the exact range here is unimportant, so long as it's large enough to trigger the "infrequent" checks
608+
"def loop():\n for i in range(1000000):\n pass\nloop()".to_owned(),
609+
&Dialect::Standard,
610+
)
611+
.unwrap();
612+
let err = eval.eval_module(ast, &globals);
613+
614+
let expected = "Evaluation cancelled";
615+
let err_msg = format!("{:#?}", err);
616+
if !err_msg.contains(expected) {
617+
panic!("Error:\n{:#?}\nExpected:\n{:?}", err, expected)
618+
}
619+
620+
Ok(())
621+
}
622+
585623
#[test]
586624
fn test_load_did_you_mean() {
587625
let mut a = Assert::new();

0 commit comments

Comments
 (0)