Merge pull request #477 from c-bata/refactor-app-py

Simplify `_app.py` by separating some storage related logic.
This commit is contained in:
c-bata
2023-05-26 19:14:05 +09:00
committed by GitHub
3 changed files with 227 additions and 189 deletions
+6 -189
View File
@@ -1,11 +1,8 @@
from __future__ import annotations
from datetime import datetime
from datetime import timedelta
import functools
import logging
import os
import threading
import typing
from typing import Any
from typing import Optional
@@ -16,18 +13,12 @@ from bottle import redirect
from bottle import request
from bottle import response
from bottle import run
from bottle import SimpleTemplate
from bottle import static_file
import optuna
from optuna.exceptions import DuplicatedStudyError
from optuna.storages import BaseStorage
from optuna.storages import RDBStorage
from optuna.study import StudyDirection
from optuna.study import StudySummary
from optuna.trial import FrozenTrial
from optuna.trial import TrialState
from optuna.version import __version__ as optuna_ver
from packaging import version
from . import _note as note
from ._bottle_util import BottleViewReturn
@@ -35,8 +26,13 @@ from ._bottle_util import json_api_view
from ._cached_extra_study_property import get_cached_extra_study_property
from ._importance import get_param_importance_from_trials_cache
from ._pareto_front import get_pareto_front_trials
from ._rdb_migration import register_rdb_migration_route
from ._serializer import serialize_study_detail
from ._serializer import serialize_study_summary
from ._storage import create_new_study
from ._storage import get_study_summaries
from ._storage import get_study_summary
from ._storage import get_trials
from ._storage_url import get_storage
from .artifact._backend import delete_all_artifacts
from .artifact._backend import register_artifact_route
@@ -46,11 +42,6 @@ if typing.TYPE_CHECKING:
from _typeshed.wsgi import WSGIApplication
from optuna_dashboard.artifact.protocol import ArtifactBackend
try:
from optuna.study._frozen import FrozenStudy
except ImportError:
FrozenStudy = None # type: ignore
logger = logging.getLogger(__name__)
@@ -60,138 +51,6 @@ STATIC_DIR = os.path.join(BASE_DIR, "public")
IMG_DIR = os.path.join(BASE_DIR, "img")
cached_path_exists = functools.lru_cache(maxsize=10)(os.path.exists)
# In-memory trials cache
trials_cache_lock = threading.Lock()
trials_cache: dict[int, list[FrozenTrial]] = {}
trials_last_fetched_at: dict[int, datetime] = {}
# RDB schema migration check
rdb_schema_migrate_lock = threading.Lock()
rdb_schema_needs_migrate = False
rdb_schema_unsupported = False
rdb_schema_template = SimpleTemplate(
"""<!DOCTYPE html>
<html lang="en">
<head>
<title>Incompatible RDB Schema Error - Optuna Dashboard</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
body {
padding: 0;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.wrapper {
padding: 64px;
width: 600px;
background-color: rgb(255, 255, 255);
box-shadow: rgba(0, 0, 0, 0.08) 0 8px 24px;
margin: 0px auto;
border-radius: 8px;
}
</style>
</head>
<body>
<div class="wrapper">
<h1>Error: Incompatible RDB Schema</h1>
% if rdb_schema_unsupported:
<p>Your Optuna version {{ optuna_ver }} seems outdated against the storage version. Please try updating optuna to the latest version by `$ pip install -U optuna`.</p>
% elif rdb_schema_needs_migrate:
<p>The runtime optuna version {{ optuna_ver }} is no longer compatible with the table schema. Please execute `$ optuna storage upgrade --storage $STORAGE_URL` or press the following button for upgrading the storage.</p>
<form action="/incompatible-rdb-schema" method="post">
<button>Migrate</button>
</form>
% end
</div>
</body>
</html>""" # noqa: E501
)
def update_schema_compatibility_flags(storage: BaseStorage) -> None:
global rdb_schema_needs_migrate, rdb_schema_unsupported
if not isinstance(storage, RDBStorage):
return
with rdb_schema_migrate_lock:
current_version = storage.get_current_version()
head_version = storage.get_head_version()
rdb_schema_needs_migrate = current_version != head_version
rdb_schema_unsupported = current_version not in storage.get_all_versions()
def get_study_summaries(storage: BaseStorage) -> list[StudySummary]:
if version.parse(optuna_ver) >= version.Version("3.0.0rc0.dev"):
frozen_studies = storage.get_all_studies() # type: ignore
if isinstance(storage, RDBStorage):
frozen_studies = sorted(frozen_studies, key=lambda s: s._study_id)
return [_frozen_study_to_study_summary(s) for s in frozen_studies]
elif version.parse(optuna_ver) >= version.Version("3.0.0b0.dev"):
return storage.get_all_study_summaries(include_best_trial=False) # type: ignore
else:
return storage.get_all_study_summaries() # type: ignore
def get_study_summary(storage: BaseStorage, study_id: int) -> Optional[StudySummary]:
summaries = get_study_summaries(storage)
for summary in summaries:
if summary._study_id != study_id:
continue
return summary
return None
def create_new_study(
storage: BaseStorage, study_name: str, directions: list[StudyDirection]
) -> int:
if version.parse(optuna_ver) >= version.Version("3.1.0.dev") and version.parse(
optuna_ver
) != version.Version("3.1.0b0"):
study_id = storage.create_new_study(directions, study_name=study_name) # type: ignore
else:
study_id = storage.create_new_study(study_name) # type: ignore
storage.set_study_directions(study_id, directions) # type: ignore
return study_id
def get_trials(storage: BaseStorage, study_id: int) -> list[FrozenTrial]:
with trials_cache_lock:
trials = trials_cache.get(study_id, None)
# Not a big fan of the heuristic, but I can't think of anything better.
if trials is None or len(trials) < 100:
ttl_seconds = 2
elif len(trials) < 500:
ttl_seconds = 5
else:
ttl_seconds = 10
last_fetched_at = trials_last_fetched_at.get(study_id, None)
if (
trials is not None
and last_fetched_at is not None
and datetime.now() - last_fetched_at < timedelta(seconds=ttl_seconds)
):
return trials
trials = storage.get_all_trials(study_id, deepcopy=False)
if (
# See https://github.com/optuna/optuna/pull/3702
version.parse(optuna_ver) <= version.Version("3.0.0rc0.dev")
and isinstance(storage, RDBStorage)
and storage.url.startswith("postgresql")
):
trials = sorted(trials, key=lambda t: t.number)
with trials_cache_lock:
trials_last_fetched_at[study_id] = datetime.now()
trials_cache[study_id] = trials
return trials
def create_app(
storage: BaseStorage,
@@ -199,7 +58,6 @@ def create_app(
debug: bool = False,
) -> Bottle:
app = Bottle()
update_schema_compatibility_flags(storage)
@app.hook("before_request")
def remove_trailing_slashes_hook() -> None:
@@ -207,39 +65,13 @@ def create_app(
@app.get("/")
def index() -> BottleViewReturn:
update_schema_compatibility_flags(storage)
if rdb_schema_needs_migrate or rdb_schema_unsupported:
return redirect("/incompatible-rdb-schema", 302)
return redirect("/dashboard", 302) # Status Found
# Accept any following paths for client-side routing
@app.get("/dashboard<:re:(/.*)?>")
def dashboard() -> BottleViewReturn:
if rdb_schema_needs_migrate or rdb_schema_unsupported:
return redirect("/incompatible-rdb-schema", 302)
return static_file("index.html", BASE_DIR, mimetype="text/html")
@app.get("/incompatible-rdb-schema")
def get_incompatible_rdb_schema() -> BottleViewReturn:
if not rdb_schema_needs_migrate and not rdb_schema_unsupported:
return redirect("/dashboard", 302)
assert isinstance(storage, RDBStorage)
return rdb_schema_template.render(
rdb_schema_needs_migrate=rdb_schema_needs_migrate,
rdb_schema_unsupported=rdb_schema_unsupported,
optuna_ver=optuna_ver,
)
@app.post("/incompatible-rdb-schema")
def post_incompatible_rdb_schema() -> BottleViewReturn:
global rdb_schema_needs_migrate
assert isinstance(storage, RDBStorage)
assert not rdb_schema_unsupported
with rdb_schema_migrate_lock:
storage.upgrade()
rdb_schema_needs_migrate = False
return redirect("/dashboard", 302)
@app.get("/api/meta")
@json_api_view
def api_meta() -> dict[str, Any]:
@@ -511,26 +343,11 @@ def create_app(
filename = gz_filename
return static_file(filename, root=STATIC_DIR)
register_rdb_migration_route(app, storage)
register_artifact_route(app, storage, artifact_backend)
return app
# TODO(c-bata): Remove type:ignore after released Optuna v3.0.0rc0.
def _frozen_study_to_study_summary(frozen_study: "FrozenStudy") -> StudySummary: # type: ignore
is_single = len(frozen_study.directions) == 1
return StudySummary(
study_name=frozen_study.study_name,
study_id=frozen_study._study_id,
direction=frozen_study.direction if is_single else None,
directions=frozen_study.directions if not is_single else None,
user_attrs=frozen_study.user_attrs,
system_attrs=frozen_study.system_attrs,
best_trial=None,
n_trials=-1, # This field isn't used by Dashboard.
datetime_start=None,
)
def run_server(
storage: Union[str, BaseStorage],
host: str = "localhost",
+112
View File
@@ -0,0 +1,112 @@
from __future__ import annotations
import threading
from bottle import Bottle
from bottle import redirect
from bottle import request
from bottle import SimpleTemplate
from optuna.storages import BaseStorage
from optuna.storages import RDBStorage
from optuna.version import __version__ as optuna_ver
from ._bottle_util import BottleViewReturn
rdb_schema_migrate_lock = threading.Lock()
rdb_schema_needs_migrate = False
rdb_schema_unsupported = False
rdb_schema_template = SimpleTemplate(
"""<!DOCTYPE html>
<html lang="en">
<head>
<title>Incompatible RDB Schema Error - Optuna Dashboard</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
body {
padding: 0;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.wrapper {
padding: 64px;
width: 600px;
background-color: rgb(255, 255, 255);
box-shadow: rgba(0, 0, 0, 0.08) 0 8px 24px;
margin: 0px auto;
border-radius: 8px;
}
</style>
</head>
<body>
<div class="wrapper">
<h1>Error: Incompatible RDB Schema</h1>
% if rdb_schema_unsupported:
<p>Your Optuna version {{ optuna_ver }} seems outdated against the storage version. Please try updating optuna to the latest version by `$ pip install -U optuna`.</p>
% elif rdb_schema_needs_migrate:
<p>The runtime optuna version {{ optuna_ver }} is no longer compatible with the table schema. Please execute `$ optuna storage upgrade --storage $STORAGE_URL` or press the following button for upgrading the storage.</p>
<form action="/incompatible-rdb-schema" method="post">
<button>Migrate</button>
</form>
% end
</div>
</body>
</html>""" # noqa: E501
)
def update_schema_compatibility_flags(storage: RDBStorage) -> None:
global rdb_schema_needs_migrate, rdb_schema_unsupported
with rdb_schema_migrate_lock:
current_version = storage.get_current_version()
head_version = storage.get_head_version()
rdb_schema_needs_migrate = current_version != head_version
rdb_schema_unsupported = current_version not in storage.get_all_versions()
def is_incompatible() -> bool:
return rdb_schema_needs_migrate or rdb_schema_unsupported
def register_rdb_migration_route(app: Bottle, storage: BaseStorage) -> None:
if isinstance(storage, RDBStorage):
update_schema_compatibility_flags(storage)
@app.get("/incompatible-rdb-schema")
def get_incompatible_rdb_schema() -> BottleViewReturn:
if not is_incompatible() or not isinstance(storage, RDBStorage):
return redirect("/dashboard", 302)
return rdb_schema_template.render(
rdb_schema_needs_migrate=rdb_schema_needs_migrate,
rdb_schema_unsupported=rdb_schema_unsupported,
optuna_ver=optuna_ver,
)
@app.post("/incompatible-rdb-schema")
def post_incompatible_rdb_schema() -> BottleViewReturn:
if not isinstance(storage, RDBStorage):
return redirect("/dashboard", 302)
global rdb_schema_needs_migrate
assert not rdb_schema_unsupported
with rdb_schema_migrate_lock:
storage.upgrade()
rdb_schema_needs_migrate = False
return redirect("/dashboard", 302)
@app.hook("before_request")
def check_schema_compatibility() -> None:
if not isinstance(storage, RDBStorage):
return
if request.path != "/" and not request.path.startswith("/dashboard"):
return
update_schema_compatibility_flags(storage)
if is_incompatible():
return redirect("/incompatible-rdb-schema", 302)
+109
View File
@@ -0,0 +1,109 @@
from __future__ import annotations
from datetime import datetime
from datetime import timedelta
import threading
import typing
from optuna.storages import BaseStorage
from optuna.storages import RDBStorage
from optuna.study import StudyDirection
from optuna.study import StudySummary
from optuna.trial import FrozenTrial
from optuna.version import __version__ as optuna_ver
from packaging import version
if typing.TYPE_CHECKING:
from optuna.study._frozen import FrozenStudy
# In-memory trials cache
trials_cache_lock = threading.Lock()
trials_cache: dict[int, list[FrozenTrial]] = {}
trials_last_fetched_at: dict[int, datetime] = {}
def get_trials(storage: BaseStorage, study_id: int) -> list[FrozenTrial]:
with trials_cache_lock:
trials = trials_cache.get(study_id, None)
# Not a big fan of the heuristic, but I can't think of anything better.
if trials is None or len(trials) < 100:
ttl_seconds = 2
elif len(trials) < 500:
ttl_seconds = 5
else:
ttl_seconds = 10
last_fetched_at = trials_last_fetched_at.get(study_id, None)
if (
trials is not None
and last_fetched_at is not None
and datetime.now() - last_fetched_at < timedelta(seconds=ttl_seconds)
):
return trials
trials = storage.get_all_trials(study_id, deepcopy=False)
if (
# See https://github.com/optuna/optuna/pull/3702
version.parse(optuna_ver) <= version.Version("3.0.0rc0.dev")
and isinstance(storage, RDBStorage)
and storage.url.startswith("postgresql")
):
trials = sorted(trials, key=lambda t: t.number)
with trials_cache_lock:
trials_last_fetched_at[study_id] = datetime.now()
trials_cache[study_id] = trials
return trials
def get_study_summaries(storage: BaseStorage) -> list[StudySummary]:
if version.parse(optuna_ver) >= version.Version("3.0.0rc0.dev"):
frozen_studies = storage.get_all_studies() # type: ignore
if isinstance(storage, RDBStorage):
frozen_studies = sorted(frozen_studies, key=lambda s: s._study_id)
return [_frozen_study_to_study_summary(s) for s in frozen_studies]
elif version.parse(optuna_ver) >= version.Version("3.0.0b0.dev"):
return storage.get_all_study_summaries(include_best_trial=False) # type: ignore
else:
return storage.get_all_study_summaries() # type: ignore
def get_study_summary(storage: BaseStorage, study_id: int) -> StudySummary | None:
summaries = get_study_summaries(storage)
for summary in summaries:
if summary._study_id != study_id:
continue
return summary
return None
def create_new_study(
storage: BaseStorage, study_name: str, directions: list[StudyDirection]
) -> int:
if version.parse(optuna_ver) >= version.Version("3.1.0.dev") and version.parse(
optuna_ver
) != version.Version("3.1.0b0"):
study_id = storage.create_new_study(directions, study_name=study_name) # type: ignore
else:
study_id = storage.create_new_study(study_name) # type: ignore
storage.set_study_directions(study_id, directions) # type: ignore
return study_id
# TODO(c-bata): Remove type:ignore after released Optuna v3.0.0rc0.
def _frozen_study_to_study_summary(frozen_study: "FrozenStudy") -> StudySummary: # type: ignore
is_single = len(frozen_study.directions) == 1
return StudySummary(
study_name=frozen_study.study_name,
study_id=frozen_study._study_id,
direction=frozen_study.direction if is_single else None,
directions=frozen_study.directions if not is_single else None,
user_attrs=frozen_study.user_attrs,
system_attrs=frozen_study.system_attrs,
best_trial=None,
n_trials=-1, # This field isn't used by Dashboard.
datetime_start=None,
)