Skip to content

Commit 4e103eb

Browse files
author
Joel Collins
committed
Added ActionView class attribute to override thread default_stop_timeout
1 parent 2cb8260 commit 4e103eb

File tree

4 files changed

+45
-6
lines changed

4 files changed

+45
-6
lines changed

src/labthings/actions/thread.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,15 @@ class ActionThread(threading.Thread):
2020
A native thread with extra functionality for tracking progress and thread termination.
2121
"""
2222

23-
def __init__(self, target=None, name=None, args=None, kwargs=None, daemon=True):
23+
def __init__(
24+
self,
25+
target=None,
26+
name=None,
27+
args=None,
28+
kwargs=None,
29+
daemon=True,
30+
default_stop_timeout: int = 5,
31+
):
2432
threading.Thread.__init__(
2533
self,
2634
group=None,
@@ -42,6 +50,7 @@ def __init__(self, target=None, name=None, args=None, kwargs=None, daemon=True):
4250
self.started = threading.Event()
4351
# Event to track if the user has requested stop
4452
self.stopping = threading.Event()
53+
self.default_stop_timeout = default_stop_timeout
4554

4655
# Make _target, _args, and _kwargs available to the subclass
4756
self._target = target
@@ -291,15 +300,18 @@ def terminate(self, exception=ActionKilledException):
291300
self.progress = None
292301
return True
293302

294-
def stop(self, timeout=5, exception=ActionKilledException):
303+
def stop(self, timeout=None, exception=ActionKilledException):
295304
"""Sets the threads internal stopped event, waits for timeout seconds for the
296305
thread to stop nicely, then forcefully kills the thread.
297306
298-
:param timeout: Time to wait before killing thread forecefully. Defaults to 5.
307+
:param timeout: Time to wait before killing thread forecefully. Defaults to ``self.default_stop_timeout``
299308
:type timeout: int
300309
:param exception: (Default value = TaskKillException)
301310
302311
"""
312+
if timeout is None:
313+
timeout = self.default_stop_timeout
314+
303315
self.stopping.set()
304316
timeout_tracker = TimeoutTracker(timeout)
305317
# While the timeout hasn't expired

src/labthings/default_views/actions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def get(self, task_id):
4242

4343
return ActionSchema().dump(task)
4444

45-
@use_args({"timeout": fields.Int(missing=5)})
45+
@use_args({"timeout": fields.Int()})
4646
def delete(self, args, task_id):
4747
"""Terminate a running task.
4848
@@ -52,7 +52,7 @@ def delete(self, args, task_id):
5252
:param task_id:
5353
5454
"""
55-
timeout = args.get("timeout", 5)
55+
timeout = args.get("timeout", None)
5656
task_dict = current_labthing().actions.to_dict()
5757

5858
if task_id not in task_dict:

src/labthings/views/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ class ActionView(View):
162162

163163
# Action handling
164164
wait_for: int = 1 # Time in seconds to wait before returning the action as pending/running
165+
default_stop_timeout: int = None # Time in seconds to wait for the action thread to end after a stop request before terminating it forcefully
165166

166167
# Internal
167168
_cls_tags = {"actions"}
@@ -278,6 +279,9 @@ def dispatch_request(self, *args, **kwargs):
278279
)
279280
# Make a task out of the views `post` method
280281
task = pool.spawn(meth, *args, **kwargs)
282+
# Optionally override the threads default_stop_timeout
283+
if self.default_stop_timeout is not None:
284+
task.default_stop_timeout = self.default_stop_timeout
281285

282286
# Keep a copy of the raw, unmarshalled JSON input in the task
283287
try:

tests/test_views.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from werkzeug.wrappers import Response as ResponseBase
44
from flask import make_response
55

6+
import time
67
import json
78

89
import pytest
@@ -157,7 +158,7 @@ def get(self):
157158
assert Index().get_value() == {"json": "body"}
158159

159160

160-
def test_action_view_get_responses(app_ctx):
161+
def test_action_view_get_responses():
161162
class Index(views.ActionView):
162163
def post(self):
163164
return {}
@@ -166,3 +167,25 @@ def post(self):
166167
assert 201 in responses
167168
assert "application/json" in responses[201]["content"]
168169
assert "schema" in responses[201]["content"]["application/json"]
170+
171+
172+
def test_action_view_stop(app):
173+
class Index(views.ActionView):
174+
default_stop_timeout = 0
175+
176+
def post(self):
177+
while True:
178+
time.sleep(1)
179+
180+
app.add_url_rule("/", view_func=Index.as_view("index"))
181+
c = app.test_client()
182+
183+
response = c.post("/")
184+
assert response.status_code == 201
185+
assert response.json.get("status") == "running"
186+
# Assert we only have a single running Action thread
187+
assert len(Index._deque) == 1
188+
action_thread = Index._deque[0]
189+
assert action_thread.default_stop_timeout == 0
190+
action_thread.stop()
191+
assert action_thread.status == "terminated"

0 commit comments

Comments
 (0)