Skip to content

Commit 0d8a8f6

Browse files
authored
use stopit to implement TimeConstrained (#1177)
If this works, maybe we can simplify the implementation of `TimeConstrained
1 parent 3ba2611 commit 0d8a8f6

File tree

4 files changed

+16
-128
lines changed

4 files changed

+16
-128
lines changed

mathics/builtin/datentime.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,7 @@
2727
from mathics.core.convert.expression import to_expression, to_mathics_list
2828
from mathics.core.convert.python import from_python
2929
from mathics.core.element import ImmutableValueMixin
30-
from mathics.core.evaluation import (
31-
Evaluation,
32-
TimeoutInterrupt,
33-
run_with_timeout_and_stack,
34-
)
30+
from mathics.core.evaluation import Evaluation
3531
from mathics.core.expression import Expression
3632
from mathics.core.list import ListExpression
3733
from mathics.core.symbols import Symbol
@@ -1070,6 +1066,7 @@ def evaluate(self, evaluation):
10701066

10711067

10721068
if sys.platform != "emscripten":
1069+
import stopit
10731070

10741071
class TimeConstrained(Builtin):
10751072
"""
@@ -1111,18 +1108,22 @@ def eval_3(self, expr, t, failexpr, evaluation):
11111108
evaluation.message("TimeConstrained", "timc", t)
11121109
return
11131110
try:
1114-
t = float(t.to_python())
1115-
evaluation.timeout_queue.append((t, datetime.now().timestamp()))
1111+
timeout = float(t.to_python())
1112+
evaluation.timeout_queue.append((timeout, datetime.now().timestamp()))
11161113
request = lambda: expr.evaluate(evaluation)
1117-
res = run_with_timeout_and_stack(request, t, evaluation)
1118-
except TimeoutInterrupt:
1119-
evaluation.timeout_queue.pop()
1120-
return failexpr.evaluate(evaluation)
1114+
done = False
1115+
with stopit.ThreadingTimeout(timeout) as to_ctx_mgr:
1116+
assert to_ctx_mgr.state == to_ctx_mgr.EXECUTING
1117+
result = request()
1118+
done = True
1119+
if done:
1120+
evaluation.timeout_queue.pop()
1121+
return result
11211122
except Exception:
11221123
evaluation.timeout_queue.pop()
11231124
raise
11241125
evaluation.timeout_queue.pop()
1125-
return res
1126+
return failexpr.evaluate(evaluation)
11261127

11271128

11281129
class TimeZone(Predefined):

mathics/core/evaluation.py

Lines changed: 1 addition & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
import sys
55
import time
66
from abc import ABC
7-
from queue import Queue
8-
from threading import Thread, stack_size as set_thread_stack_size
97
from typing import Any, Callable, Dict, List, Optional, Tuple, Union, overload
108

119
from mathics_scanner import TranslateError
@@ -57,16 +55,6 @@
5755
SymbolPrePrint = Symbol("System`$PrePrint")
5856
SymbolPost = Symbol("System`$Post")
5957

60-
61-
def _thread_target(request, queue) -> None:
62-
try:
63-
result = request()
64-
queue.put((True, result))
65-
except BaseException:
66-
exc_info = sys.exc_info()
67-
queue.put((False, exc_info))
68-
69-
7058
# MAX_RECURSION_DEPTH gives the maximum value allowed for $RecursionLimit. it's usually set to its
7159
# default settings.DEFAULT_MAX_RECURSION_DEPTH.
7260

@@ -96,54 +84,6 @@ def set_python_recursion_limit(n) -> None:
9684
raise OverflowError
9785

9886

99-
def run_with_timeout_and_stack(request, timeout, evaluation):
100-
"""
101-
interrupts evaluation after a given time period. Provides a suitable stack environment.
102-
"""
103-
104-
# only use set_thread_stack_size if max recursion depth was changed via the environment variable
105-
# MATHICS_MAX_RECURSION_DEPTH. if it is set, we always use a thread, even if timeout is None, in
106-
# order to be able to set the thread stack size.
107-
108-
if MAX_RECURSION_DEPTH > settings.DEFAULT_MAX_RECURSION_DEPTH:
109-
set_thread_stack_size(python_stack_size(MAX_RECURSION_DEPTH))
110-
elif timeout is None:
111-
return request()
112-
113-
queue = Queue(maxsize=1) # stores the result or exception
114-
thread = Thread(target=_thread_target, args=(request, queue))
115-
thread.start()
116-
117-
# Thead join(timeout) can leave zombie threads (we are the parent)
118-
# when a time out occurs, but the thread hasn't terminated. See
119-
# https://docs.python.org/3/library/multiprocessing.shared_memory.html
120-
# for a detailed discussion of this.
121-
#
122-
# To reduce this problem, we make use of specific properties of
123-
# the Mathics3 evaluator: if we set "evaluation.timeout", the
124-
# next call to "Expression.evaluate" in the thread will finish it
125-
# immediately.
126-
#
127-
# However this still will not terminate long-running processes
128-
# in Sympy or or libraries called by Mathics3 that might hang or run
129-
# for a long time.
130-
thread.join(timeout)
131-
if thread.is_alive():
132-
evaluation.timeout = True
133-
while thread.is_alive():
134-
time.sleep(0.001)
135-
pass
136-
evaluation.timeout = False
137-
evaluation.stopped = False
138-
raise TimeoutInterrupt()
139-
140-
success, result = queue.get()
141-
if success:
142-
return result
143-
else:
144-
raise result[1].with_traceback(result[2])
145-
146-
14787
class _Out(KeyComparable):
14888
def __init__(self) -> None:
14989
self.is_message = False
@@ -296,7 +236,7 @@ def evaluate():
296236

297237
try:
298238
try:
299-
result = run_with_timeout_and_stack(evaluate, timeout, self)
239+
result = evaluate()
300240
except KeyboardInterrupt:
301241
if self.catch_interrupt:
302242
self.exc_result = SymbolAborted

mathics/eval/sympy.py

Lines changed: 1 addition & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@
22
Evaluation of SymPy functions
33
"""
44

5-
import sys
6-
from queue import Queue
7-
from threading import Thread
85
from typing import Optional
96

107
import sympy
@@ -15,9 +12,7 @@
1512
from mathics.core.evaluation import Evaluation
1613

1714

18-
def eval_sympy_unconstrained(
19-
self, z: BaseElement, evaluation: Evaluation
20-
) -> Optional[BaseElement]:
15+
def eval_sympy(self, z: BaseElement, evaluation: Evaluation) -> Optional[BaseElement]:
2116
"""
2217
Evaluate element `z` converting it to SymPy and back to Mathics3.
2318
If an exception is raised we return None.
@@ -33,52 +28,3 @@ def eval_sympy_unconstrained(
3328
return from_sympy(tracing.run_sympy(sympy_fn, *sympy_args))
3429
except Exception:
3530
return
36-
37-
38-
def eval_sympy_with_timeout(
39-
self, z: BaseElement, evaluation: Evaluation
40-
) -> Optional[BaseElement]:
41-
"""
42-
Evaluate an element `z` converting it to SymPy,
43-
and back to Mathics3.
44-
If an exception is raised we return None.
45-
46-
This version is run in a thread, and checked for evaluation timeout.
47-
"""
48-
49-
if evaluation.timeout is None:
50-
return eval_sympy_unconstrained(self, z, evaluation)
51-
52-
def _thread_target(queue) -> None:
53-
try:
54-
result = eval_sympy_unconstrained(self, z, evaluation)
55-
queue.put((True, result))
56-
except BaseException:
57-
exc_info = sys.exc_info()
58-
queue.put((False, exc_info))
59-
60-
queue = Queue(maxsize=1) # stores the result or exception
61-
62-
thread = Thread(target=_thread_target, args=(queue,))
63-
thread.start()
64-
while thread.is_alive():
65-
thread.join(0.001)
66-
if evaluation.timeout:
67-
# I can kill the thread.
68-
# just leave it...
69-
return None
70-
71-
# pick the result and return
72-
success, result = queue.get()
73-
if success:
74-
return result
75-
else:
76-
raise result[1].with_traceback(result[2])
77-
78-
79-
# Common top-level evaluation SymPy "eval" function:
80-
eval_sympy = (
81-
eval_sympy_unconstrained
82-
if sys.platform in ("emscripten",)
83-
else eval_sympy_with_timeout
84-
)

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ dependencies = [
1818
# Pillow 9.1.0 supports BigTIFF with big-endian byte order.
1919
# ExampleData image hedy.tif is in this format.
2020
# Pillow 9.2 handles sunflowers.jpg
21+
"stopit; platform_system != 'Emscripten'",
2122
"pillow >= 9.2",
2223
"pint",
2324
"python-dateutil",

0 commit comments

Comments
 (0)