:interrupt-fn[!WARNING] This is an experimental feature that may change based on user feedback. Keep an eye on the CHANGELOG for changes.
SCI supports passing a zero arg :interrupt-fn to its options, which is called on every fn
(and loop/recur) body entry. Using this hook you can cancel evaluation after a
certain number of iterations, check Thread/interrupted, etc. A demo:
(require '[sci.interrupt :as interrupt])
(defn limit [n]
(let [counter (atom 0)]
(fn [] (when (> (swap! counter inc) n)
(interrupt/interrupt!)))))
(sci/eval-string "(loop [] (recur))" {:interrupt-fn (limit 1000000)})
;;=> throws "Interrupted"
This above limit function limits the number of iterations. The logic is up to
you. You can make one that limits wall clock time:
(defn time-limit [ms]
(let [deadline (+ (System/currentTimeMillis) ms)]
(fn []
(when (> (System/currentTimeMillis) deadline)
(interrupt/interrupt!)))))
(sci/eval-string "(loop [] (recur))" {:interrupt-fn (time-limit 1000)})
;;=> throws "Interrupted" after ~1 second
or one that polls Thread/.isInterrupted:
(defn thread-limit []
(fn []
(when (.isInterrupted (Thread/currentThread))
(interrupt/interrupt!))))
(let [fut (future (sci/eval-string "(loop [] (recur))" {:interrupt-fn (thread-limit)}))]
(Thread/sleep 1000)
(future-cancel fut))
;;=> the running evaluation throws "Interrupted"
The interrupt-fn is executed on every fn body entrance, so it’s worthwile to optimize performance.
Note that :interrupt-fn only fires on interpreted code. Core functions in SCI are mostly called via the host and are not interpreted. E.g. (doall (range)) would not hit :interrupt-fn.
The namespace sci.interrupt provides interruptible core function drop-in alternatives. These are currently not loaded by default. You can opt-in on those by using the clojure-core configuration and adding it to :namespaces {'clojure.core ...}:
Note that the core overrides can introduce performance regressions in your code compared to the standard SCI clojure core functions.
(sci/eval-string "(reduce + (range))"
{:interrupt-fn (limit 1000000)
:namespaces {'clojure.core interrupt/clojure-core}})
;;=> throws "Interrupted"
Host functions that you expose yourself are not automatically made aware of
:interrupt-fn, this must be done manually. You can get the :interrupt-fn
from a ctx using the sci.interrupt/get-interrupt-fn accessor.
(require '[sci.ctx-store :as store])
;; A host function that loops
(defn my-host-loop [n]
(let [interrupt-fn (interrupt/get-interrupt-fn (store/get-ctx))]
(dotimes [_ n]
(when interrupt-fn (interrupt-fn))
;; ... work ...
)))
(sci/eval-string "(my-host-loop 1000000)"
{:interrupt-fn (limit 500)
:namespaces {'user {'my-host-loop my-host-loop}}})
;;=> throws "Interrupted"
Note that even with these overrides, unbounded programs are still possible.
For hard guarantees it is best to run untrusted code in a separate process that can be killed.
Here is an example of how your program can run unboundedly even with :interrupt-fn.
Since the long-running CPU-heavy call is made in the host, :interrupt-fn never hits and your program will hang.
(sci/eval-string "(.pow (biginteger 10) 100000000)"
{:interrupt-fn (limit 500)
:classes {:allow :all}
:namespaces {'clojure.core interrupt/clojure-core}})