77from typing import Any , Callable , Dict , Iterable , Optional
88
99from flask import copy_current_request_context , has_request_context , request
10- from werkzeug .exceptions import BadRequest
10+ from werkzeug .exceptions import BadRequest , HTTPException
1111
1212from ..deque import LockableDeque
1313from ..utilities import TimeoutTracker
@@ -22,6 +22,42 @@ class ActionKilledException(SystemExit):
2222class ActionThread (threading .Thread ):
2323 """
2424 A native thread with extra functionality for tracking progress and thread termination.
25+
26+ Arguments:
27+ * `action` is the name of the action that's running
28+ * `target`, `name`, `args`, `kwargs` and `daemon` are passed to `threading.Thread`
29+ (though the defualt for `daemon` is changed to `True`)
30+ * `default_stop_timeout` specifies how long we wait for the `target` function to
31+ stop nicely (e.g. by checking the `stopping` Event )
32+ * `log_len` gives the number of log entries before we start dumping them
33+ * `http_error_lock` allows the calling thread to handle some
34+ errors initially. See below.
35+
36+ ## Error propagation
37+ If the `target` function throws an Exception, by default this will result in:
38+ * The thread terminating
39+ * The Action's status being set to `error`
40+ * The exception appearing in the logs with a traceback
41+ * The exception being raised in the background thread.
42+ However, `HTTPException` subclasses are used in Flask/Werkzeug web apps to
43+ return HTTP status codes indicating specific errors, and so merit being
44+ handled differently.
45+
46+ Normally, when an Action is initiated, the thread handling the HTTP request
47+ does not return immediately - it waits for a short period to check whether
48+ the Action has completed or returned an error. If an HTTPError is raised
49+ in the Action thread before the initiating thread has sent an HTTP response,
50+ we **don't** want to propagate the error here, but instead want to re-raise
51+ it in the calling thread. This will then mean that the HTTP request is
52+ answered with the appropriate error code, rather than returning a `201`
53+ code, along with a description of the task (showing that it was successfully
54+ started, but also showing that it subsequently failed with an error).
55+
56+ In order to activate this behaviour, we must pass in a `threading.Lock`
57+ object. This lock should already be acquired by the request-handling
58+ thread. If an error occurs, and this lock is acquired, the exception
59+ should not be re-raised until the calling thread has had the chance to deal
60+ with it.
2561 """
2662
2763 def __init__ (
@@ -34,6 +70,7 @@ def __init__(
3470 daemon : bool = True ,
3571 default_stop_timeout : int = 5 ,
3672 log_len : int = 100 ,
73+ http_error_lock : Optional [threading .Lock ] = None
3774 ):
3875 threading .Thread .__init__ (
3976 self ,
@@ -56,6 +93,8 @@ def __init__(
5693 # Event to track if the user has requested stop
5794 self .stopping : threading .Event = threading .Event ()
5895 self .default_stop_timeout : int = default_stop_timeout
96+ # Allow the calling thread to handle HTTP errors for a short time at the start
97+ self .http_error_lock = http_error_lock or threading .Lock ()
5998
6099 # Make _target, _args, and _kwargs available to the subclass
61100 self ._target : Optional [Callable ] = target
@@ -220,16 +259,32 @@ def wrapped(*args, **kwargs):
220259 # Set state to stopped
221260 self ._status = "cancelled"
222261 self .progress = None
262+ except HTTPException as e :
263+ self ._exception = e
264+ # If the lock is acquired elsewhere, assume the error
265+ # will be handled there.
266+ if self .http_error_lock .acquire (blocking = False ):
267+ self .http_error_lock .release ()
268+ logging .error (
269+ "An HTTPException occurred in an action thread, but "
270+ "the parent request was no longer waiting for it."
271+ )
272+ logging .error (traceback .format_exc ())
273+ raise e
274+ else :
275+ logging .info (f"Propagating { e } back to request handler" )
223276 except Exception as e : # skipcq: PYL-W0703
224- logging .error (traceback .format_exc ())
225- self ._return_value = str (e )
226- self ._status = "error"
227277 self ._exception = e
278+ logging .error (traceback .format_exc ())
228279 raise e
229280 finally :
230281 self ._end_time = datetime .datetime .now ()
231282 logging .getLogger ().removeHandler (handler ) # Stop logging this thread
232283 # If we don't remove the handler, it's a memory leak.
284+ if self ._exception :
285+ self ._return_value = str (self ._exception )
286+ self ._status = "error"
287+
233288
234289 return wrapped
235290
0 commit comments