From fab5fc489915cee8cc50be43dcaa78f090f5b879 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 23 Nov 2015 17:35:25 +0100 Subject: [PATCH] Added preemptive caching of / and /i18n//messages.js Introduced a @preemptively_cached decorator that for decorated views persists the provided data in ~/.octoprint/data/preemptive_flask_cache.yaml in a list indexed by the view's path if the data is not yet part of the list. During initialization the server will iterate over the persisted paths and data and for each persisted path and entry in the list initialize a temporary WSGI environment based on the data (which is interpretated as keyword arguments to werkzeug's EnvironmentBuilder) which will then be used to call the view function in the correct context. The current implementation for / and /i18n//messages.js utilizes that decorator to allow preemptive caching of those views (/ being probably the most expensive one in the whole core application) utilizing request base URLs (internal access, external access, reverse proxy with prefix url etc) that had been encountered in the past. Through the new config setting server.preemptiveCaching.exceptions it is possible to define a set of base URLs to never cache. Preemptive caching can be globally disabled by setting devel.cache.preemptive to false. --- src/octoprint/server/__init__.py | 30 +++++++++++++- src/octoprint/server/util/flask.py | 63 ++++++++++++++++++++++++++++++ src/octoprint/server/views.py | 9 +++-- src/octoprint/settings.py | 6 ++- 4 files changed, 103 insertions(+), 5 deletions(-) diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py index 643e32cc..83014e53 100644 --- a/src/octoprint/server/__init__.py +++ b/src/octoprint/server/__init__.py @@ -467,6 +467,10 @@ class Server(): implementation.on_after_startup() pluginLifecycleManager.add_callback("enabled", call_on_after_startup) + # when we are through with that we also run our preemptive cache + if settings().getBoolean(["devel", "cache", "preemptive"]): + self._execute_preemptive_flask_caching() + import threading threading.Thread(target=work).start() ioloop.add_callback(on_after_startup) @@ -526,7 +530,7 @@ class Server(): if default_language is not None and not default_language == "_default" and default_language in LANGUAGES: return Locale.negotiate([default_language], LANGUAGES) - return request.accept_languages.best_match(LANGUAGES) + return Locale.parse(request.accept_languages.best_match(LANGUAGES)) def _setup_logging(self, debug, logConf=None): defaultConfig = { @@ -663,6 +667,30 @@ class Server(): self._register_template_plugins() + def _execute_preemptive_flask_caching(self): + from octoprint.server.util.flask import get_preemptive_cache_data + from werkzeug.test import EnvironBuilder + + cache_data = get_preemptive_cache_data() + if not cache_data: + return + + def execute_caching(): + for route in sorted(cache_data.keys(), key=lambda x: (x.count("/"), x)): + entries = cache_data[route] + for kwargs in entries: + try: + self._logger.info("Preemptively caching {} for {!r}".format(route, kwargs)) + builder = EnvironBuilder(**kwargs) + app(builder.get_environ(), lambda *a, **kw: None) + except: + self._logger.exception("Error while trying to preemptively cache {} for {!r}".format(route, kwargs)) + + import threading + cache_thread = threading.Thread(target=execute_caching, name="Preemptive Cache Worker") + cache_thread.daemon = True + cache_thread.start() + def _register_template_plugins(self): template_plugins = pluginManager.get_implementations(octoprint.plugin.TemplatePlugin) for plugin in template_plugins: diff --git a/src/octoprint/server/util/flask.py b/src/octoprint/server/util/flask.py index ce59f6d6..0a83f690 100644 --- a/src/octoprint/server/util/flask.py +++ b/src/octoprint/server/util/flask.py @@ -376,6 +376,69 @@ def cache_check_response_headers(response): return False +_preemptive_flask_cache = "preemptive_flask_cache.yaml" + +def preemptively_cached(data, unless=None): + def decorator(f): + @functools.wraps(f) + def decorated_function(*args, **kwargs): + if not (callable(unless) and unless()): + entry_data = data + if callable(entry_data): + entry_data = entry_data() + + if entry_data is not None: + from flask import request + from octoprint.util import atomic_write + import yaml + + data_folder = settings().getBaseFolder("data") + cache_data_file = os.path.join(data_folder, _preemptive_flask_cache) + cache_data = get_preemptive_cache_data() + + if not request.path in cache_data: + cache_data[request.path] = [] + + cache_data_for_path = cache_data.get(request.path, []) + if all(map(lambda entry: entry_data != entry, cache_data_for_path)): + logging.getLogger(__name__).info("Adding {} for {!r} to views to preemptively cache".format(request.path, entry_data)) + cache_data[request.path] = cache_data_for_path + [entry_data] + try: + with atomic_write(cache_data_file, "wb", prefix="octoprint-{}-".format(_preemptive_flask_cache[:-len(".yaml")]), suffix=".yaml") as handle: + yaml.safe_dump(cache_data, handle,default_flow_style=False, indent=" ", allow_unicode=True) + except: + logging.getLogger(__name__).exception("Error while writing {}".format(_preemptive_flask_cache)) + + return f(*args, **kwargs) + + return decorated_function + + return decorator + + +def get_preemptive_cache_data(root=None): + import yaml + + data_folder = settings().getBaseFolder("data") + cache_data_file = os.path.join(data_folder, _preemptive_flask_cache) + if not os.path.isfile(cache_data_file): + return dict() + + cache_data = None + try: + with open(cache_data_file, "r") as f: + cache_data = yaml.safe_load(f) + except: + logging.getLogger(__name__).exception("Error while reading {}".format(_preemptive_flask_cache)) + + if cache_data is None: + cache_data = dict() + + if root: + return cache_data.get(root, dict()) + else: + return cache_data + def add_non_caching_response_headers(response): response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0" diff --git a/src/octoprint/server/views.py b/src/octoprint/server/views.py index 3399a740..34d701a8 100644 --- a/src/octoprint/server/views.py +++ b/src/octoprint/server/views.py @@ -29,10 +29,11 @@ _valid_div_re = re.compile("[a-zA-Z_-]+") @app.route("/") @util.flask.cached(timeout=-1, refreshif=lambda: util.flask.cache_check_headers() or "_refresh" in request.values, - key=lambda: "view:{}:{}".format(request.base_url, g.locale), + key=lambda: "view:{}:{}".format(request.base_url, g.locale.language if g.locale else "default"), unless_response=util.flask.cache_check_response_headers) +@util.flask.preemptively_cached(data=lambda: dict(path=request.path, base_url=request.url_root, query_string="l10n={}".format(g.locale.language)) if g.locale else None, + unless=lambda: request.url_root in settings().get(["server", "preemptiveCache", "exceptions"])) def index(): - #~~ a bunch of settings enable_gcodeviewer = settings().getBoolean(["gcodeViewer", "enabled"]) @@ -404,7 +405,9 @@ def robotsTxt(): @app.route("/i18n//.js") @util.flask.cached(timeout=-1, refreshif=lambda: util.flask.cache_check_headers() or "_refresh" in request.values, - key=lambda: "view:{}:{}".format(request.base_url, g.locale)) + key=lambda: "view:{}".format(request.base_url)) +@util.flask.preemptively_cached(data=lambda: dict(path=request.path, base_url=request.url_root) if g.locale else None, + unless=lambda: request.url_root in settings().get(["server", "preemptiveCache", "exceptions"])) def localeJs(locale, domain): messages = dict() plural_expr = None diff --git a/src/octoprint/settings.py b/src/octoprint/settings.py index a7137e14..ffd972db 100644 --- a/src/octoprint/settings.py +++ b/src/octoprint/settings.py @@ -114,6 +114,9 @@ default_settings = { "diskspace": { "warning": 500 * 1024 * 1024, # 500 MB "critical": 200 * 1024 * 1024, # 200 MB + }, + "preemptiveCache": { + "exceptions": [] } }, "webcam": { @@ -262,7 +265,8 @@ default_settings = { "devel": { "stylesheet": "css", "cache": { - "enabled": True + "enabled": True, + "preemptive": True }, "webassets": { "minify": False,