Summary
When a Starlark script returns a callable (the "auto-invoke main()" idiom), the evaluator calls it on a bare starlark.Thread with no context-cancellation wiring and no Print handler. A long-running or infinite auto-invoked function ignores ctx deadlines, and its print() output bypasses the configured logger.
Root cause
engines/starlark/evaluator/evaluator.go:210-223:
if callable, ok := result.Value.(starlarkLib.Callable); ok {
thread := &starlarkLib.Thread{Name: "func"} // no AfterFunc/Cancel, no Print
val, err := starlarkLib.Call(thread, callable, nil, nil)
...
}
The top-level exec path (evaluator.go:97-110) wires both:
thread := &starlarkLib.Thread{Name: "eval", Print: func(...) { logger.InfoContext(...) }}
stop := context.AfterFunc(ctx, func() { thread.Cancel(ctx.Err().Error()) })
defer stop()
The auto-call thread has neither, so thread.Cancel is never invoked when ctx is done.
Reproduction (confirmed — measured)
With a 100 ms timeout context and a returned function running a long loop, the auto-called function ran ~19.5 s to completion and Eval returned success — the expired context was never observed. Control: the same loop at top level (inside prog.Init) aborts at ~100 ms with a cancellation error.
Impact
High — the documented "return a function to run it" pattern is exempt from the timeout and cancellation guarantees the top-level path provides, and its diagnostic output escapes the logger.
Fix
Use the same thread setup on the call path: attach the Print handler and wire context.AfterFunc(ctx, ...) → thread.Cancel(...) with defer stop(). Optionally pre-check ctx.Err() before the call. Add a test asserting a cancelled context aborts an auto-invoked function.
Summary
When a Starlark script returns a callable (the "auto-invoke
main()" idiom), the evaluator calls it on a barestarlark.Threadwith no context-cancellation wiring and noPrinthandler. A long-running or infinite auto-invoked function ignoresctxdeadlines, and itsprint()output bypasses the configured logger.Root cause
engines/starlark/evaluator/evaluator.go:210-223:The top-level
execpath (evaluator.go:97-110) wires both:The auto-call thread has neither, so
thread.Cancelis never invoked whenctxis done.Reproduction (confirmed — measured)
With a 100 ms timeout context and a returned function running a long loop, the auto-called function ran ~19.5 s to completion and
Evalreturned success — the expired context was never observed. Control: the same loop at top level (insideprog.Init) aborts at ~100 ms with a cancellation error.Impact
High — the documented "return a function to run it" pattern is exempt from the timeout and cancellation guarantees the top-level path provides, and its diagnostic output escapes the logger.
Fix
Use the same thread setup on the call path: attach the
Printhandler and wirecontext.AfterFunc(ctx, ...) → thread.Cancel(...)withdefer stop(). Optionally pre-checkctx.Err()before the call. Add a test asserting a cancelled context aborts an auto-invoked function.