|
| 1 | +import ctypes |
1 | 2 | import inspect
|
2 | 3 | import json
|
3 | 4 | import random
|
4 | 5 | import time
|
| 6 | +from contextlib import contextmanager |
5 | 7 | from functools import wraps
|
| 8 | +from threading import Timer, current_thread |
6 | 9 | from traceback import format_exception
|
7 |
| -from typing import Any, Callable, TypeVar |
| 10 | +from typing import Any, Callable, Iterator, TypeVar |
8 | 11 |
|
9 | 12 | from django.utils.crypto import RANDOM_STRING_CHARS
|
10 | 13 | from typing_extensions import ParamSpec
|
11 | 14 |
|
| 15 | +from .exceptions import TimeoutException |
| 16 | + |
12 | 17 | T = TypeVar("T")
|
13 | 18 | P = ParamSpec("P")
|
14 | 19 |
|
@@ -74,3 +79,40 @@ def get_random_id() -> str:
|
74 | 79 | it's not cryptographically secure.
|
75 | 80 | """
|
76 | 81 | return "".join(random.choices(RANDOM_STRING_CHARS, k=32))
|
| 82 | + |
| 83 | + |
| 84 | +def _do_timeout(tid: int) -> None: |
| 85 | + """ |
| 86 | + Raise `TimeoutException` in the given thread. |
| 87 | +
|
| 88 | + Here be dragons. |
| 89 | + """ |
| 90 | + # Since we're in Python, no GIL lock is needed |
| 91 | + ret = ctypes.pythonapi.PyThreadState_SetAsyncExc( |
| 92 | + ctypes.c_ulong(tid), ctypes.py_object(TimeoutException) |
| 93 | + ) |
| 94 | + |
| 95 | + if ret == 0: |
| 96 | + raise RuntimeError("Timeout failed - thread not found") |
| 97 | + elif ret != 1: |
| 98 | + ret = ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_ulong(tid), None) |
| 99 | + raise RuntimeError("Timeout failed") |
| 100 | + |
| 101 | + |
| 102 | +@contextmanager |
| 103 | +def timeout(timeout: float) -> Iterator[Timer]: |
| 104 | + """ |
| 105 | + Run the wrapped code for at most `timeout` seconds before aborting. |
| 106 | +
|
| 107 | + This works by starting a timer thread, and using "magic" raises an exception |
| 108 | + in the main process after the timer expires. |
| 109 | +
|
| 110 | + Raises `TimeoutException` when the timeout occurs. |
| 111 | + """ |
| 112 | + timeout_timer = Timer(timeout, _do_timeout, args=[current_thread().ident]) |
| 113 | + try: |
| 114 | + timeout_timer.start() |
| 115 | + |
| 116 | + yield timeout_timer |
| 117 | + finally: |
| 118 | + timeout_timer.cancel() |
0 commit comments