7
7
from typing import Any , Callable , Dict , Iterable , Optional
8
8
9
9
from flask import copy_current_request_context , has_request_context , request
10
- from werkzeug .exceptions import BadRequest
10
+ from werkzeug .exceptions import BadRequest , HTTPException
11
11
12
12
from ..deque import LockableDeque
13
13
from ..utilities import TimeoutTracker
@@ -22,6 +22,42 @@ class ActionKilledException(SystemExit):
22
22
class ActionThread (threading .Thread ):
23
23
"""
24
24
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.
25
61
"""
26
62
27
63
def __init__ (
@@ -34,6 +70,7 @@ def __init__(
34
70
daemon : bool = True ,
35
71
default_stop_timeout : int = 5 ,
36
72
log_len : int = 100 ,
73
+ http_error_lock : Optional [threading .Lock ] = None
37
74
):
38
75
threading .Thread .__init__ (
39
76
self ,
@@ -56,6 +93,8 @@ def __init__(
56
93
# Event to track if the user has requested stop
57
94
self .stopping : threading .Event = threading .Event ()
58
95
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 ()
59
98
60
99
# Make _target, _args, and _kwargs available to the subclass
61
100
self ._target : Optional [Callable ] = target
@@ -220,16 +259,32 @@ def wrapped(*args, **kwargs):
220
259
# Set state to stopped
221
260
self ._status = "cancelled"
222
261
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" )
223
276
except Exception as e : # skipcq: PYL-W0703
224
- logging .error (traceback .format_exc ())
225
- self ._return_value = str (e )
226
- self ._status = "error"
227
277
self ._exception = e
278
+ logging .error (traceback .format_exc ())
228
279
raise e
229
280
finally :
230
281
self ._end_time = datetime .datetime .now ()
231
282
logging .getLogger ().removeHandler (handler ) # Stop logging this thread
232
283
# 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
+
233
288
234
289
return wrapped
235
290
0 commit comments