From 03a48265cfda727171deef70537ecc98a613610b Mon Sep 17 00:00:00 2001 From: c-bata Date: Thu, 17 Feb 2022 20:52:27 +0900 Subject: [PATCH 1/5] Add --server option for Gunicorn support --- Dockerfile | 4 ++-- README.md | 12 +++++++----- optuna_dashboard/cli.py | 6 +++++- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/Dockerfile b/Dockerfile index 392e1dd9..bc5b4150 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,7 +15,7 @@ FROM python:3.8-buster AS python-builder WORKDIR /usr/src RUN pip install --upgrade pip setuptools -RUN pip install --progress-bar off PyMySQL[rsa] psycopg2-binary +RUN pip install --progress-bar off PyMySQL[rsa] psycopg2-binary gunicorn ADD ./setup.cfg /usr/src/setup.cfg ADD ./setup.py /usr/src/setup.py @@ -32,5 +32,5 @@ RUN mkdir /app WORKDIR /app EXPOSE 8080 -ENTRYPOINT ["optuna-dashboard", "--port", "8080", "--host", "0.0.0.0"] +ENTRYPOINT ["optuna-dashboard", "--port", "8080", "--host", "0.0.0.0", "--server", "gunicorn"] diff --git a/README.md b/README.md index 8f4865cf..48651652 100644 --- a/README.md +++ b/README.md @@ -43,11 +43,13 @@ positional arguments: storage DB URL (e.g. sqlite:///example.db) optional arguments: - -h, --help show this help message and exit - --port PORT port number (default: 8080) - --host HOST hostname (default: 127.0.0.1) - --version, -v show program's version number and exit - --quiet, -q quiet + -h, --help show this help message and exit + --port PORT port number (default: 8080) + --host HOST hostname (default: 127.0.0.1) + --server {wsgiref,gunicorn} + server (default: wsgiref) + --version, -v show program's version number and exit + --quiet, -q quiet ``` diff --git a/optuna_dashboard/cli.py b/optuna_dashboard/cli.py index 146b3218..08dcd1e1 100644 --- a/optuna_dashboard/cli.py +++ b/optuna_dashboard/cli.py @@ -11,6 +11,7 @@ from .app import create_app AUTO_RELOAD = os.environ.get("OPTUNA_DASHBOARD_AUTO_RELOAD") == "1" +SERVER_CHOICES = ["wsgiref", "gunicorn"] def main() -> None: @@ -22,6 +23,9 @@ def main() -> None: parser.add_argument( "--host", help="hostname (default: %(default)s)", default="127.0.0.1" ) + parser.add_argument( + "--server", help="server (default: %(default)s)", default="wsgiref", choices=SERVER_CHOICES + ) parser.add_argument("--version", "-v", action="version", version=__version__) parser.add_argument("--quiet", "-q", help="quiet", action="store_true") args = parser.parse_args() @@ -33,7 +37,7 @@ def main() -> None: storage = RDBStorage(args.storage) app = create_app(storage) - run(app, host=args.host, port=args.port, quiet=args.quiet, reloader=AUTO_RELOAD) + run(app, host=args.host, port=args.port, server=args.server, quiet=args.quiet, reloader=AUTO_RELOAD) if __name__ == "__main__": From 2eeebf9cac5f14a156acb4224e8ded41749c0870 Mon Sep 17 00:00:00 2001 From: c-bata Date: Thu, 17 Feb 2022 21:00:34 +0900 Subject: [PATCH 2/5] Update README --- README.md | 46 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 48651652..cca4c769 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,23 @@ Listening on http://localhost:8080/ Hit Ctrl-C to quit. ``` +
+ +Running optuna-dashboard with Gunicorn + +optuna-dashboard uses [wsgiref](https://docs.python.org/3/library/wsgiref.html) module +which is provided as a Python standard library. But it has not been reviewed for security +issues, so not suitable for the production use. You can run optuna-dashboard with Gunicorn +more secure and/or more fast. + +```console +$ pip install gunicorn +$ optuna-dashboard sqlite:///db.sqlite3 --server gunicorn +``` + +
+ +
More command line options @@ -54,6 +71,21 @@ optional arguments:
+
+ +Python Interface + +`run_server(storage: Union[str, BaseStorage], host: str = 'localhost', port: int = 8080) -> NoReturn` + +Start running optuna-dashboard and blocks until the server terminates. +This function uses wsgiref module which is not intended for the production use. + +`wsgi(storage: Union[str, BaseStorage]) -> WSGIApplication` + +This function exposes WSGI interface for people who want to run on the +production-class WSGI servers like Gunicorn or uWSGI. + +
## Using an official Docker image @@ -104,20 +136,6 @@ You can walk-through trials by filtering and sorting. ![optuna-dashboard-trials-datagrid](https://user-images.githubusercontent.com/5564044/114265667-20d57d00-9a2d-11eb-8b9c-69541c9b4a28.gif) -## Python Interface - -### `run_server(storage: Union[str, BaseStorage], host: str = 'localhost', port: int = 8080) -> NoReturn` - -Start running optuna-dashboard and blocks until the server terminates. -This function uses wsgiref module which is not intended for the production -use. If you want to run optuna-dashboard more secure and/or more fast, -please use WSGI server like Gunicorn or uWSGI via `wsgi()` function. - -### `wsgi(storage: Union[str, BaseStorage]) -> WSGIApplication` - -This function exposes WSGI interface for people who want to run on the -production-class WSGI servers like Gunicorn or uWSGI. - ## Submitting patches If you want to contribute, please check [Developers Guide](./DEVELOPMENT.md). From 0d68c5b8c88a1c20202c164ef8930d07ca64141d Mon Sep 17 00:00:00 2001 From: c-bata Date: Thu, 17 Feb 2022 21:03:52 +0900 Subject: [PATCH 3/5] Fix a lint error --- README.md | 4 ++-- optuna_dashboard/cli.py | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index cca4c769..d68e9556 100644 --- a/README.md +++ b/README.md @@ -75,12 +75,12 @@ optional arguments: Python Interface -`run_server(storage: Union[str, BaseStorage], host: str = 'localhost', port: int = 8080) -> NoReturn` +**`run_server(storage: Union[str, BaseStorage], host: str = 'localhost', port: int = 8080) -> NoReturn`** Start running optuna-dashboard and blocks until the server terminates. This function uses wsgiref module which is not intended for the production use. -`wsgi(storage: Union[str, BaseStorage]) -> WSGIApplication` +**`wsgi(storage: Union[str, BaseStorage]) -> WSGIApplication`** This function exposes WSGI interface for people who want to run on the production-class WSGI servers like Gunicorn or uWSGI. diff --git a/optuna_dashboard/cli.py b/optuna_dashboard/cli.py index 08dcd1e1..a95c8242 100644 --- a/optuna_dashboard/cli.py +++ b/optuna_dashboard/cli.py @@ -24,7 +24,10 @@ def main() -> None: "--host", help="hostname (default: %(default)s)", default="127.0.0.1" ) parser.add_argument( - "--server", help="server (default: %(default)s)", default="wsgiref", choices=SERVER_CHOICES + "--server", + help="server (default: %(default)s)", + default="wsgiref", + choices=SERVER_CHOICES, ) parser.add_argument("--version", "-v", action="version", version=__version__) parser.add_argument("--quiet", "-q", help="quiet", action="store_true") @@ -37,7 +40,14 @@ def main() -> None: storage = RDBStorage(args.storage) app = create_app(storage) - run(app, host=args.host, port=args.port, server=args.server, quiet=args.quiet, reloader=AUTO_RELOAD) + run( + app, + host=args.host, + port=args.port, + server=args.server, + quiet=args.quiet, + reloader=AUTO_RELOAD, + ) if __name__ == "__main__": From 306ac92da504e3dc2b7f8c9e969330668e285514 Mon Sep 17 00:00:00 2001 From: c-bata Date: Thu, 17 Feb 2022 21:27:05 +0900 Subject: [PATCH 4/5] Update DEVELOPMENT.md --- DEVELOPMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index cd274679..955a1354 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -40,7 +40,7 @@ You can use the Docker image like below: ``` # SQLite3 -$ docker run -it --rm -p 8080:8080 -v `PWD`:/app -w /app optuna-dashboard sqlite:///db.sqlite3 +$ docker run -it --rm -p 8080:8080 -v `PWD`:/app -w /app sqlite:///db.sqlite3 ``` ### Running dashboard server From 66e47ff78e189d185026761a5c7e8cb69277e56f Mon Sep 17 00:00:00 2001 From: c-bata Date: Thu, 17 Feb 2022 22:12:35 +0900 Subject: [PATCH 5/5] Fix gunicorn runner --- optuna_dashboard/cli.py | 46 +++++++++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/optuna_dashboard/cli.py b/optuna_dashboard/cli.py index a95c8242..87f43dfe 100644 --- a/optuna_dashboard/cli.py +++ b/optuna_dashboard/cli.py @@ -1,6 +1,8 @@ import argparse import os +from typing import NoReturn +from bottle import Bottle from bottle import run from optuna.storages import BaseStorage from optuna.storages import RDBStorage @@ -14,7 +16,35 @@ AUTO_RELOAD = os.environ.get("OPTUNA_DASHBOARD_AUTO_RELOAD") == "1" SERVER_CHOICES = ["wsgiref", "gunicorn"] -def main() -> None: +def run_wsgiref(app: Bottle, host: str, port: int, quiet: bool) -> NoReturn: # type: ignore + run( + app, + host=host, + port=port, + server="wsgiref", + quiet=quiet, + reloader=AUTO_RELOAD, + ) + + +def run_gunicorn(app: Bottle, host: str, port: int, quiet: bool) -> NoReturn: # type: ignore + # See https://docs.gunicorn.org/en/latest/custom.html + + from gunicorn.app.base import BaseApplication + + class _Application(BaseApplication): + def load_config(self) -> None: + self.cfg.set("bind", f"{host}:{port}") + if quiet: + self.cfg.set("loglevel", "error") + + def load(self) -> Bottle: + return app + + _Application().run() + + +def main() -> NoReturn: parser = argparse.ArgumentParser(description="Real-time dashboard for Optuna.") parser.add_argument("storage", help="DB URL (e.g. sqlite:///example.db)", type=str) parser.add_argument( @@ -40,14 +70,12 @@ def main() -> None: storage = RDBStorage(args.storage) app = create_app(storage) - run( - app, - host=args.host, - port=args.port, - server=args.server, - quiet=args.quiet, - reloader=AUTO_RELOAD, - ) + if args.server == "wsgiref": + run_wsgiref(app, args.host, args.port, args.quiet) + elif args.server == "gunicorn": + run_gunicorn(app, args.host, args.port, args.quiet) + else: + raise Exception("must not reach here") if __name__ == "__main__":