Added `use_werkzeug_server` option to `backend.http`.

There are situations where you may not want to run the HTTP server in a
full blown WSGI-over-Tornado container - unit/integration tests and
embedded single-core devices are among those cases.

In those scenarios, we should allow the user to be able to run the
backend using the built-in Werkzeug server provided by Flask.
This commit is contained in:
Fabio Manganiello 2023-05-20 15:26:58 +02:00
parent 013274bcbc
commit 666ea9ea6b
Signed by: blacklight
GPG Key ID: D90FBA7F76362774
3 changed files with 48 additions and 22 deletions

View File

@ -199,6 +199,7 @@ class HttpBackend(Backend):
resource_dirs: Optional[Mapping[str, str]] = None, resource_dirs: Optional[Mapping[str, str]] = None,
secret_key_file: Optional[str] = None, secret_key_file: Optional[str] = None,
num_workers: Optional[int] = None, num_workers: Optional[int] = None,
use_werkzeug_server: bool = False,
**kwargs, **kwargs,
): ):
""" """
@ -211,6 +212,16 @@ class HttpBackend(Backend):
:param secret_key_file: Path to the file containing the secret key that will be used by Flask :param secret_key_file: Path to the file containing the secret key that will be used by Flask
(default: ``~/.local/share/platypush/flask.secret.key``). (default: ``~/.local/share/platypush/flask.secret.key``).
:param num_workers: Number of worker processes to use (default: ``(cpu_count * 2) + 1``). :param num_workers: Number of worker processes to use (default: ``(cpu_count * 2) + 1``).
:param use_werkzeug_server: Whether the backend should be served by a
Werkzeug server (default: ``False``). Note that using the built-in
Werkzeug server instead of Tornado is very inefficient, and it
doesn't support websocket-based features either so the UI will
probably be severely limited. You should only use this option if:
- You are running tests.
- You have issues with running a full Tornado server - for
example, you are running the application on a small embedded
device that doesn't support Tornado.
""" """
super().__init__(**kwargs) super().__init__(**kwargs)
@ -235,6 +246,7 @@ class HttpBackend(Backend):
) )
self.local_base_url = f'http://localhost:{self.port}' self.local_base_url = f'http://localhost:{self.port}'
self.num_workers = num_workers or (cpu_count() * 2) + 1 self.num_workers = num_workers or (cpu_count() * 2) + 1
self.use_werkzeug_server = use_werkzeug_server
def send_message(self, *_, **__): def send_message(self, *_, **__):
self.logger.warning('Use cURL or any HTTP client to query the HTTP backend') self.logger.warning('Use cURL or any HTTP client to query the HTTP backend')
@ -339,14 +351,25 @@ class HttpBackend(Backend):
self.num_workers, self.num_workers,
) )
sockets = bind_sockets(self.port, address=self.bind_address, reuse_port=True) if self.use_werkzeug_server:
application.config['redis_queue'] = self.bus.redis_queue
application.run(
host=self.bind_address,
port=self.port,
use_reloader=False,
debug=True,
)
else:
sockets = bind_sockets(
self.port, address=self.bind_address, reuse_port=True
)
try: try:
fork_processes(self.num_workers) fork_processes(self.num_workers)
future = self._post_fork_main(sockets) future = self._post_fork_main(sockets)
asyncio.run(future) asyncio.run(future)
except (asyncio.CancelledError, KeyboardInterrupt): except (asyncio.CancelledError, KeyboardInterrupt):
return return
def _start_web_server(self): def _start_web_server(self):
self._server_proc = Process(target=self._web_server_proc) self._server_proc = Process(target=self._web_server_proc)

View File

@ -1,9 +1,10 @@
import logging import logging
import os import os
import pytest
import time import time
from threading import Thread from threading import Thread
import pytest
from platypush import Daemon, Config from platypush import Daemon, Config
from .utils import config_file, set_base_url from .utils import config_file, set_base_url
@ -17,8 +18,6 @@ def clear_loggers():
This is to prevent pytest spitting out logging errors on teardown if the logging objects have been deinitialized This is to prevent pytest spitting out logging errors on teardown if the logging objects have been deinitialized
(see https://github.com/pytest-dev/pytest/issues/5502#issuecomment-647157873). (see https://github.com/pytest-dev/pytest/issues/5502#issuecomment-647157873).
""" """
import logging
# noinspection PyUnresolvedReferences
loggers = [logging.getLogger()] + list(logging.Logger.manager.loggerDict.values()) loggers = [logging.getLogger()] + list(logging.Logger.manager.loggerDict.values())
for logger in loggers: for logger in loggers:
handlers = getattr(logger, 'handlers', []) handlers = getattr(logger, 'handlers', [])
@ -31,31 +30,34 @@ def app():
logging.info('Starting Platypush test service') logging.info('Starting Platypush test service')
Config.init(config_file) Config.init(config_file)
app = Daemon(config_file=config_file, redis_queue='platypush-tests/bus') _app = Daemon(config_file=config_file, redis_queue='platypush-tests/bus')
Thread(target=lambda: app.run()).start() Thread(target=_app.run).start()
logging.info('Sleeping {} seconds while waiting for the daemon to start up'.format(app_start_timeout)) logging.info(
'Sleeping %d seconds while waiting for the daemon to start up',
app_start_timeout,
)
time.sleep(app_start_timeout) time.sleep(app_start_timeout)
yield app yield _app
logging.info('Stopping Platypush test service') logging.info('Stopping Platypush test service')
app.stop_app() _app.stop_app()
clear_loggers() clear_loggers()
db_file = (Config.get('main.db') or {}).get('engine', '')[len('sqlite:///'):] db = (Config.get('main.db') or {}).get('engine', '')[len('sqlite:///') :]
if db_file and os.path.isfile(db_file): if db and os.path.isfile(db):
logging.info('Removing temporary db file {}'.format(db_file)) logging.info('Removing temporary db file %s', db)
os.unlink(db_file) os.unlink(db)
@pytest.fixture(scope='session') @pytest.fixture(scope='session')
def db_file(): def db_file():
yield Config.get('main.db')['engine'][len('sqlite:///'):] yield Config.get('main.db')['engine'][len('sqlite:///') :]
@pytest.fixture(scope='session') @pytest.fixture(scope='session')
def base_url(): def base_url():
backends = Config.get_backends() backends = Config.get_backends()
assert 'http' in backends, 'Missing HTTP server configuration' assert 'http' in backends, 'Missing HTTP server configuration'
url = 'http://localhost:{port}'.format(port=backends['http']['port']) url = f'http://localhost:{backends["http"]["port"]}'
set_base_url(url) set_base_url(url)
yield url yield url

View File

@ -6,7 +6,8 @@ main.db:
backend.http: backend.http:
port: 8123 port: 8123
disable_websocket: True num_workers: 1
use_werkzeug_server: True
backend.redis: backend.redis:
disabled: False disabled: False