Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

use stopit to implement TimeConstrained #1177

Merged
merged 4 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 13 additions & 12 deletions mathics/builtin/datentime.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,7 @@
from mathics.core.convert.expression import to_expression, to_mathics_list
from mathics.core.convert.python import from_python
from mathics.core.element import ImmutableValueMixin
from mathics.core.evaluation import (
Evaluation,
TimeoutInterrupt,
run_with_timeout_and_stack,
)
from mathics.core.evaluation import Evaluation
from mathics.core.expression import Expression
from mathics.core.list import ListExpression
from mathics.core.symbols import Symbol
Expand Down Expand Up @@ -1070,6 +1066,7 @@ def evaluate(self, evaluation):


if sys.platform != "emscripten":
import stopit

class TimeConstrained(Builtin):
"""
Expand Down Expand Up @@ -1111,18 +1108,22 @@ def eval_3(self, expr, t, failexpr, evaluation):
evaluation.message("TimeConstrained", "timc", t)
return
try:
t = float(t.to_python())
evaluation.timeout_queue.append((t, datetime.now().timestamp()))
timeout = float(t.to_python())
evaluation.timeout_queue.append((timeout, datetime.now().timestamp()))
request = lambda: expr.evaluate(evaluation)
res = run_with_timeout_and_stack(request, t, evaluation)
except TimeoutInterrupt:
evaluation.timeout_queue.pop()
return failexpr.evaluate(evaluation)
done = False
with stopit.ThreadingTimeout(timeout) as to_ctx_mgr:
assert to_ctx_mgr.state == to_ctx_mgr.EXECUTING
result = request()
done = True
if done:
evaluation.timeout_queue.pop()
return result
except Exception:
evaluation.timeout_queue.pop()
raise
evaluation.timeout_queue.pop()
return res
return failexpr.evaluate(evaluation)


class TimeZone(Predefined):
Expand Down
62 changes: 1 addition & 61 deletions mathics/core/evaluation.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
import sys
import time
from abc import ABC
from queue import Queue
from threading import Thread, stack_size as set_thread_stack_size
from typing import Any, Callable, Dict, List, Optional, Tuple, Union, overload

from mathics_scanner import TranslateError
Expand Down Expand Up @@ -57,16 +55,6 @@
SymbolPrePrint = Symbol("System`$PrePrint")
SymbolPost = Symbol("System`$Post")


def _thread_target(request, queue) -> None:
try:
result = request()
queue.put((True, result))
except BaseException:
exc_info = sys.exc_info()
queue.put((False, exc_info))


# MAX_RECURSION_DEPTH gives the maximum value allowed for $RecursionLimit. it's usually set to its
# default settings.DEFAULT_MAX_RECURSION_DEPTH.

Expand Down Expand Up @@ -96,54 +84,6 @@ def set_python_recursion_limit(n) -> None:
raise OverflowError


def run_with_timeout_and_stack(request, timeout, evaluation):
"""
interrupts evaluation after a given time period. Provides a suitable stack environment.
"""

# only use set_thread_stack_size if max recursion depth was changed via the environment variable
# MATHICS_MAX_RECURSION_DEPTH. if it is set, we always use a thread, even if timeout is None, in
# order to be able to set the thread stack size.

if MAX_RECURSION_DEPTH > settings.DEFAULT_MAX_RECURSION_DEPTH:
set_thread_stack_size(python_stack_size(MAX_RECURSION_DEPTH))
elif timeout is None:
return request()

queue = Queue(maxsize=1) # stores the result or exception
thread = Thread(target=_thread_target, args=(request, queue))
thread.start()

# Thead join(timeout) can leave zombie threads (we are the parent)
# when a time out occurs, but the thread hasn't terminated. See
# https://docs.python.org/3/library/multiprocessing.shared_memory.html
# for a detailed discussion of this.
#
# To reduce this problem, we make use of specific properties of
# the Mathics3 evaluator: if we set "evaluation.timeout", the
# next call to "Expression.evaluate" in the thread will finish it
# immediately.
#
# However this still will not terminate long-running processes
# in Sympy or or libraries called by Mathics3 that might hang or run
# for a long time.
thread.join(timeout)
if thread.is_alive():
evaluation.timeout = True
while thread.is_alive():
time.sleep(0.001)
pass
evaluation.timeout = False
evaluation.stopped = False
raise TimeoutInterrupt()

success, result = queue.get()
if success:
return result
else:
raise result[1].with_traceback(result[2])


class _Out(KeyComparable):
def __init__(self) -> None:
self.is_message = False
Expand Down Expand Up @@ -296,7 +236,7 @@ def evaluate():

try:
try:
result = run_with_timeout_and_stack(evaluate, timeout, self)
result = evaluate()
except KeyboardInterrupt:
if self.catch_interrupt:
self.exc_result = SymbolAborted
Expand Down
56 changes: 1 addition & 55 deletions mathics/eval/sympy.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@
Evaluation of SymPy functions
"""

import sys
from queue import Queue
from threading import Thread
from typing import Optional

import sympy
Expand All @@ -15,9 +12,7 @@
from mathics.core.evaluation import Evaluation


def eval_sympy_unconstrained(
self, z: BaseElement, evaluation: Evaluation
) -> Optional[BaseElement]:
def eval_sympy(self, z: BaseElement, evaluation: Evaluation) -> Optional[BaseElement]:
"""
Evaluate element `z` converting it to SymPy and back to Mathics3.
If an exception is raised we return None.
Expand All @@ -33,52 +28,3 @@ def eval_sympy_unconstrained(
return from_sympy(tracing.run_sympy(sympy_fn, *sympy_args))
except Exception:
return


def eval_sympy_with_timeout(
self, z: BaseElement, evaluation: Evaluation
) -> Optional[BaseElement]:
"""
Evaluate an element `z` converting it to SymPy,
and back to Mathics3.
If an exception is raised we return None.

This version is run in a thread, and checked for evaluation timeout.
"""

if evaluation.timeout is None:
return eval_sympy_unconstrained(self, z, evaluation)

def _thread_target(queue) -> None:
try:
result = eval_sympy_unconstrained(self, z, evaluation)
queue.put((True, result))
except BaseException:
exc_info = sys.exc_info()
queue.put((False, exc_info))

queue = Queue(maxsize=1) # stores the result or exception

thread = Thread(target=_thread_target, args=(queue,))
thread.start()
while thread.is_alive():
thread.join(0.001)
if evaluation.timeout:
# I can kill the thread.
# just leave it...
return None

# pick the result and return
success, result = queue.get()
if success:
return result
else:
raise result[1].with_traceback(result[2])


# Common top-level evaluation SymPy "eval" function:
eval_sympy = (
eval_sympy_unconstrained
if sys.platform in ("emscripten",)
else eval_sympy_with_timeout
)
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ dependencies = [
# Pillow 9.1.0 supports BigTIFF with big-endian byte order.
# ExampleData image hedy.tif is in this format.
# Pillow 9.2 handles sunflowers.jpg
"stopit; platform_system != 'Emscripten'",
"pillow >= 9.2",
"pint",
"python-dateutil",
Expand Down