Skip to content

Optional interruption support#1039

Open
whilo wants to merge 5 commits into
babashka:masterfrom
whilo:resource-check
Open

Optional interruption support#1039
whilo wants to merge 5 commits into
babashka:masterfrom
whilo:resource-check

Conversation

@whilo
Copy link
Copy Markdown

@whilo whilo commented Apr 16, 2026

Adds :interrupt-fn callback for per-context execution bounding (fixes #1038).

An optional :interrupt-fn in the SCI context options is called on every function entry. The function can throw to abort execution, enabling iteration limits, memory limits, and external timeout checks — independently per context.

The check is captured as a closed-over local at function-creation time, so it is a free nil test when not configured: zero overhead for existing users.

Coverage: loop/recur, dotimes, while, direct self-calls, mutual recursion. merge-opts and fork preserve the callback.


whilo added 4 commits April 16, 2026 13:23
Adds an optional :resource-check function to SCI contexts. When provided,
it is called on every loop/recur iteration and every fn-call dispatch.
The function can throw to abort execution.

This enables:
- Iteration limits (prevent infinite loops)
- Memory limits (via JVM getThreadAllocatedBytes in the callback)
- External timeout (via Thread.interrupted check in the callback)
- Per-context bounds (different sandboxes get different limits)

The check is cached at function creation time (not looked up per call).
When nil (default), zero overhead — the `when` branch is never taken.

Overhead with amortized check (every 10K iterations): ~2.5x on tight loops.
With every 100K: ~10%. For real workloads: negligible.

Changes:
- opts.cljc: add :resource-check field to Ctx record, wire through init
- fns.cljc: call resource-check on each recur in gen-fn macro
- evaluator.cljc: call resource-check on fn-call dispatch
- Rename the option to :interrupt-fn throughout (opts, fns, evaluator)
- Move the check to the top of gen-fn's loop: fires on every function
  entry (initial call) AND every recur, covering direct recursion,
  mutual recursion, dotimes, while, and loop/recur uniformly
- Remove the check from fn-call: it was only reachable for 20+ arg
  calls (gen-return-call generates direct (f ...) calls for 0-19 args),
  so the original placement was both insufficient and added overhead for
  existing users — now fn-call is truly zero-cost for nil :interrupt-fn
- Add rc# capture to arity-many (20+ arg) fallback in gen-fn
- Fix merge-opts to preserve :interrupt-fn when not overridden
- Add interrupt_fn_test.cljc covering recur, dotimes, direct recursion,
  mutual recursion, nil default, fork, and merge-opts propagation
…ll/dorun/count/into/reduce

When :interrupt-fn is provided, opts/init installs interruptible versions
of nine clojure.core functions that would otherwise bypass the interrupt
mechanism by running entirely host-side:

  Producers:     range, repeat, cycle, iterate
  Materializers: doall, dorun, count, into, reduce

Each wrapper calls store/get-ctx at invocation time to read :interrupt-fn,
so fork and merge-opts work correctly. When :interrupt-fn is absent the
original host functions are used unchanged — zero overhead for existing users.

counted? collections (vectors, maps, sets) take the fast O(1) path in count.
reduce supports reduced for early termination.
Comment thread src/sci/impl/evaluator.cljc
Comment thread src/sci/impl/fns.cljc Outdated
- Restore unrelated TODO and prn debug comments in fn-call macro that
  were accidentally removed when the resource-check block was removed.
- Use (when (some? interrupt-fn#) ...) instead of (when interrupt-fn# ...)
  in gen-fn hot paths — faster on CLJS, semantically equivalent since
  interrupt-fn# is always either a fn or nil.
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.

External interruption support

2 participants