2013-01-29 22:19:05 +01:00
|
|
|
import ctypes
|
2022-04-05 20:34:56 +02:00
|
|
|
import logging
|
2020-06-11 00:54:34 +02:00
|
|
|
import sys
|
2013-01-29 21:47:53 +01:00
|
|
|
import threading
|
2020-06-11 00:54:34 +02:00
|
|
|
import time
|
|
|
|
from types import TracebackType
|
2021-02-16 00:56:23 +01:00
|
|
|
from typing import Callable, Optional, Tuple, Type, TypeVar
|
2013-01-29 21:47:53 +01:00
|
|
|
|
2020-03-27 01:32:21 +01:00
|
|
|
# Based on https://code.activestate.com/recipes/483752/
|
2013-01-29 21:47:53 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2022-11-17 09:30:48 +01:00
|
|
|
class TimeoutExpiredError(Exception):
|
2021-02-12 08:20:45 +01:00
|
|
|
"""Exception raised when a function times out."""
|
2016-11-29 07:22:02 +01:00
|
|
|
|
2017-11-05 11:15:10 +01:00
|
|
|
def __str__(self) -> str:
|
2021-02-12 08:20:45 +01:00
|
|
|
return "Function call timed out."
|
2013-01-29 21:47:53 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2021-02-12 08:20:45 +01:00
|
|
|
ResultT = TypeVar("ResultT")
|
2016-06-29 00:14:07 +02:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2021-02-16 00:56:23 +01:00
|
|
|
def timeout(timeout: float, func: Callable[[], ResultT]) -> ResultT:
|
2021-02-12 08:19:30 +01:00
|
|
|
"""Call the function in a separate thread.
|
|
|
|
Return its return value, or raise an exception,
|
|
|
|
within approximately 'timeout' seconds.
|
2013-01-29 21:47:53 +01:00
|
|
|
|
2022-11-17 09:30:48 +01:00
|
|
|
The function may receive a TimeoutExpiredError exception
|
2021-02-12 08:19:30 +01:00
|
|
|
anywhere in its code, which could have arbitrary
|
|
|
|
unsafe effects (resources not released, etc.).
|
|
|
|
It might also fail to receive the exception and
|
|
|
|
keep running in the background even though
|
|
|
|
timeout() has returned.
|
2013-01-29 21:47:53 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
This may also fail to interrupt functions which are
|
|
|
|
stuck in a long-running primitive interpreter
|
|
|
|
operation."""
|
2013-01-29 21:47:53 +01:00
|
|
|
|
|
|
|
class TimeoutThread(threading.Thread):
|
2017-11-05 11:15:10 +01:00
|
|
|
def __init__(self) -> None:
|
2013-01-29 21:47:53 +01:00
|
|
|
threading.Thread.__init__(self)
|
python: Convert assignment type annotations to Python 3.6 style.
This commit was split by tabbott; this piece covers the vast majority
of files in Zulip, but excludes scripts/, tools/, and puppet/ to help
ensure we at least show the right error messages for Xenial systems.
We can likely further refine the remaining pieces with some testing.
Generated by com2ann, with whitespace fixes and various manual fixes
for runtime issues:
- invoiced_through: Optional[LicenseLedger] = models.ForeignKey(
+ invoiced_through: Optional["LicenseLedger"] = models.ForeignKey(
-_apns_client: Optional[APNsClient] = None
+_apns_client: Optional["APNsClient"] = None
- notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- signup_notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ signup_notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- author: Optional[UserProfile] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
+ author: Optional["UserProfile"] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
- bot_owner: Optional[UserProfile] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
+ bot_owner: Optional["UserProfile"] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
- default_sending_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
- default_events_register_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_sending_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_events_register_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
-descriptors_by_handler_id: Dict[int, ClientDescriptor] = {}
+descriptors_by_handler_id: Dict[int, "ClientDescriptor"] = {}
-worker_classes: Dict[str, Type[QueueProcessingWorker]] = {}
-queues: Dict[str, Dict[str, Type[QueueProcessingWorker]]] = {}
+worker_classes: Dict[str, Type["QueueProcessingWorker"]] = {}
+queues: Dict[str, Dict[str, Type["QueueProcessingWorker"]]] = {}
-AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional[LDAPSearch] = None
+AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional["LDAPSearch"] = None
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-22 01:09:50 +02:00
|
|
|
self.result: Optional[ResultT] = None
|
|
|
|
self.exc_info: Tuple[
|
|
|
|
Optional[Type[BaseException]],
|
|
|
|
Optional[BaseException],
|
|
|
|
Optional[TracebackType],
|
|
|
|
] = (None, None, None)
|
2013-01-29 21:47:53 +01:00
|
|
|
|
|
|
|
# Don't block the whole program from exiting
|
|
|
|
# if this is the only thread left.
|
|
|
|
self.daemon = True
|
|
|
|
|
2017-11-05 11:15:10 +01:00
|
|
|
def run(self) -> None:
|
2013-01-29 21:47:53 +01:00
|
|
|
try:
|
2021-02-16 00:56:23 +01:00
|
|
|
self.result = func()
|
2013-05-01 21:59:56 +02:00
|
|
|
except BaseException:
|
|
|
|
self.exc_info = sys.exc_info()
|
2013-01-29 21:47:53 +01:00
|
|
|
|
2017-11-05 11:15:10 +01:00
|
|
|
def raise_async_timeout(self) -> None:
|
2022-04-05 20:39:12 +02:00
|
|
|
# This function is called from another thread; we attempt
|
2022-11-17 09:30:48 +01:00
|
|
|
# to raise a TimeoutExpiredError in _this_ thread.
|
2022-04-05 20:39:12 +02:00
|
|
|
assert self.ident is not None
|
2022-04-05 20:23:57 +02:00
|
|
|
ctypes.pythonapi.PyThreadState_SetAsyncExc(
|
2022-10-19 21:58:33 +02:00
|
|
|
ctypes.c_ulong(self.ident),
|
2022-11-17 09:30:48 +01:00
|
|
|
ctypes.py_object(TimeoutExpiredError),
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
2013-01-29 22:19:05 +01:00
|
|
|
|
2013-01-29 21:47:53 +01:00
|
|
|
thread = TimeoutThread()
|
|
|
|
thread.start()
|
|
|
|
thread.join(timeout)
|
|
|
|
|
2016-07-03 14:37:59 +02:00
|
|
|
if thread.is_alive():
|
2022-04-05 20:39:12 +02:00
|
|
|
# We need to retry, because an async exception received while
|
|
|
|
# the thread is in a system call is simply ignored.
|
2015-11-01 17:15:05 +01:00
|
|
|
for i in range(10):
|
2013-01-29 22:19:05 +01:00
|
|
|
thread.raise_async_timeout()
|
|
|
|
time.sleep(0.1)
|
2016-07-03 14:37:59 +02:00
|
|
|
if not thread.is_alive():
|
2013-01-29 22:19:05 +01:00
|
|
|
break
|
2022-04-05 20:29:47 +02:00
|
|
|
if thread.exc_info[1] is not None:
|
|
|
|
# Re-raise the exception we sent, if possible, so the
|
|
|
|
# stacktrace originates in the slow code
|
|
|
|
raise thread.exc_info[1].with_traceback(thread.exc_info[2])
|
2022-04-05 20:34:56 +02:00
|
|
|
# If we don't have that for some reason (e.g. we failed to
|
|
|
|
# kill it), just raise from here; the thread _may still be
|
|
|
|
# running_ because it failed to see any of our exceptions, and
|
|
|
|
# we just ignore it.
|
|
|
|
if thread.is_alive():
|
|
|
|
logging.warning("Failed to time out backend thread")
|
2022-11-17 09:30:48 +01:00
|
|
|
raise TimeoutExpiredError
|
2013-01-29 22:19:05 +01:00
|
|
|
|
2020-04-09 22:04:49 +02:00
|
|
|
if thread.exc_info[1] is not None:
|
2022-04-05 20:29:47 +02:00
|
|
|
# Died with some other exception; re-raise it
|
2020-04-09 22:04:49 +02:00
|
|
|
raise thread.exc_info[1].with_traceback(thread.exc_info[2])
|
2022-04-05 20:39:12 +02:00
|
|
|
|
|
|
|
assert thread.result is not None
|
2013-01-29 21:47:53 +01:00
|
|
|
return thread.result
|