Skip to content

Commit 6e09fed

Browse files
authored
Merge pull request #1963 from plotly/1780-test-thread-shutdown
Replace deprecated flask shutdown with thread kill.
2 parents 6ee2328 + a3bbd74 commit 6e09fed

File tree

2 files changed

+36
-16
lines changed

2 files changed

+36
-16
lines changed

Diff for: CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22
All notable changes to `dash` will be documented in this file.
33
This project adheres to [Semantic Versioning](https://semver.org/).
44

5+
## [Unreleased]
6+
7+
### Fixed
8+
9+
- [#1963](https://github.com/plotly/dash/pull/1963) Fix [#1780](https://github.com/plotly/dash/issues/1780) flask shutdown deprecation warning when running dashduo threaded tests.
10+
511
## [2.3.0] - 2022-03-13
612

713
### Added

Diff for: dash/testing/application_runners.py

+30-16
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
import subprocess
88
import logging
99
import inspect
10+
import ctypes
1011

1112
import runpy
12-
import flask
1313
import requests
1414

1515
from dash.testing.errors import NoAppFoundError, TestingTimeoutError, ServerCloseError
@@ -102,6 +102,26 @@ def tmp_app_path(self):
102102
return self._tmp_app_path
103103

104104

105+
class StoppableThread(threading.Thread):
106+
def get_id(self): # pylint: disable=R1710
107+
if hasattr(self, "_thread_id"):
108+
return self._thread_id
109+
for thread_id, thread in threading._active.items(): # pylint: disable=W0212
110+
if thread is self:
111+
return thread_id
112+
113+
def kill(self):
114+
thread_id = self.get_id()
115+
res = ctypes.pythonapi.PyThreadState_SetAsyncExc(
116+
ctypes.c_long(thread_id), ctypes.py_object(SystemExit)
117+
)
118+
if res == 0:
119+
raise ValueError(f"Invalid thread id: {thread_id}")
120+
if res > 1:
121+
ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(thread_id), None)
122+
raise SystemExit("Stopping thread failure")
123+
124+
105125
class ThreadedRunner(BaseDashRunner):
106126
"""Runs a dash application in a thread.
107127
@@ -110,25 +130,14 @@ class ThreadedRunner(BaseDashRunner):
110130

111131
def __init__(self, keep_open=False, stop_timeout=3):
112132
super().__init__(keep_open=keep_open, stop_timeout=stop_timeout)
113-
self.stop_route = "/_stop-{}".format(uuid.uuid4().hex)
114133
self.thread = None
115134

116-
@staticmethod
117-
def _stop_server():
118-
# https://werkzeug.palletsprojects.com/en/0.15.x/serving/#shutting-down-the-server
119-
stopper = flask.request.environ.get("werkzeug.server.shutdown")
120-
if stopper is None:
121-
raise RuntimeError("Not running with the Werkzeug Server")
122-
stopper()
123-
return "Flask server is shutting down"
124-
125135
# pylint: disable=arguments-differ
126136
def start(self, app, **kwargs):
127137
"""Start the app server in threading flavor."""
128-
app.server.add_url_rule(self.stop_route, self.stop_route, self._stop_server)
129138

130139
def _handle_error():
131-
self._stop_server()
140+
self.stop()
132141

133142
app.server.errorhandler(500)(_handle_error)
134143

@@ -139,9 +148,13 @@ def run():
139148
kwargs["port"] = self.port
140149
else:
141150
self.port = kwargs["port"]
142-
app.run_server(threaded=True, **kwargs)
143151

144-
self.thread = threading.Thread(target=run)
152+
try:
153+
app.run_server(threaded=True, **kwargs)
154+
except SystemExit:
155+
logger.info("Server stopped")
156+
157+
self.thread = StoppableThread(target=run)
145158
self.thread.daemon = True
146159
try:
147160
self.thread.start()
@@ -155,7 +168,8 @@ def run():
155168
wait.until(lambda: self.accessible(self.url), timeout=1)
156169

157170
def stop(self):
158-
requests.get("{}{}".format(self.url, self.stop_route))
171+
self.thread.kill()
172+
self.thread.join()
159173
wait.until_not(self.thread.is_alive, self.stop_timeout)
160174

161175

0 commit comments

Comments
 (0)