From 7ed71fad289baa411f98a2f07ae8db123beafb80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Fri, 24 Jun 2016 14:28:19 +0200 Subject: [PATCH] Improved request and preemptive caching * UiPlugins may now disable preemptive caching * Preemptive cache recording will also be disabled when preemptive caching is disabled according to settings (so far only execution on startup was prevented then) * Preemptive cache forces configured view. E.g. if plugin "someplugin" was recorded in the preemptive cache, a custom context now makes sure that only that this view will be cached even if request parameters are hard to define to get routing to work correctly for an alternative UI --- src/octoprint/plugin/types.py | 10 +++++ src/octoprint/server/__init__.py | 19 +++++++++- src/octoprint/server/util/flask.py | 16 ++++++-- src/octoprint/server/views.py | 61 +++++++++++++++++++++++------- 4 files changed, 87 insertions(+), 19 deletions(-) diff --git a/src/octoprint/plugin/types.py b/src/octoprint/plugin/types.py index 2a284483..4cf435fc 100644 --- a/src/octoprint/plugin/types.py +++ b/src/octoprint/plugin/types.py @@ -741,6 +741,16 @@ class UiPlugin(OctoPrintPlugin, SortablePlugin): """ return None + def get_ui_preemptive_caching_enabled(self): + """ + Allows to control whether the view provided by the plugin should be preemptively + cached on server startup (default) or not. + + Returns: + bool: Whether to enable preemptive caching or not + """ + return True + def get_ui_data_for_preemptive_caching(self): """ Allows defining additional data to be persisted in the preemptive cache configuration, on diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py index 37c631e4..74bbbb27 100644 --- a/src/octoprint/server/__init__.py +++ b/src/octoprint/server/__init__.py @@ -752,6 +752,20 @@ class Server(object): entries = reversed(sorted(cache_data[route], key=lambda x: x.get("_count", 0))) for kwargs in entries: plugin = kwargs.get("plugin", None) + if plugin: + try: + plugin_info = pluginManager.get_plugin_info(plugin, require_enabled=True) + implementation = plugin_info.implementation + if implementation is None or not isinstance(implementation, octoprint.plugin.UiPlugin): + self._logger.debug("Plugin {} is not a UiPlugin, preemptive caching makes no sense".format(plugin)) + continue + if not implementation.get_ui_preemptive_caching_enabled(): + self._logger.debug("Plugin {} has disabled preemptive caching".format(plugin)) + continue + except: + self._logger.exception("Error while trying to check if plugin {} has preemptive caching enabled, skipping entry") + continue + additional_request_data = kwargs.get("_additional_request_data", dict()) kwargs = dict((k, v) for k, v in kwargs.items() if not k.startswith("_") and not k == "plugin") kwargs.update(additional_request_data) @@ -761,8 +775,9 @@ class Server(object): else: self._logger.info("Preemptively caching {} for {!r}".format(route, kwargs)) builder = EnvironBuilder(**kwargs) - with preemptive_cache.disable_access_logging(): - app(builder.get_environ(), lambda *a, **kw: None) + with preemptive_cache.cache_environment(dict(plugin=plugin if plugin is not None else "_default")): + with preemptive_cache.disable_access_logging(): + app(builder.get_environ(), lambda *a, **kw: None) except: self._logger.exception("Error while trying to preemptively cache {} for {!r}".format(route, kwargs)) diff --git a/src/octoprint/server/util/flask.py b/src/octoprint/server/util/flask.py index d175bb77..8c7c3a18 100644 --- a/src/octoprint/server/util/flask.py +++ b/src/octoprint/server/util/flask.py @@ -383,11 +383,14 @@ class PreemptiveCache(object): def __init__(self, cachefile): self.cachefile = cachefile + self.environment = None - self._lock = threading.RLock() self._logger = logging.getLogger(__name__ + "." + self.__class__.__name__) self._log_access = True + self._lock = threading.RLock() + self._environment_lock = threading.RLock() + def record(self, data, unless=None): if callable(unless) and unless(): return @@ -407,6 +410,13 @@ class PreemptiveCache(object): yield self._log_access = True + @contextlib.contextmanager + def cache_environment(self, environment): + with self._environment_lock: + self.environment = environment + yield + self.environment = None + def clean_all_data(self, cleanup_function): assert callable(cleanup_function) @@ -585,13 +595,13 @@ def conditional(condition, met): def check_etag(etag): return flask.request.method in ("GET", "HEAD") and \ - flask.request.if_none_match and \ + flask.request.if_none_match is not None and \ etag in flask.request.if_none_match def check_lastmodified(lastmodified): return flask.request.method in ("GET", "HEAD") and \ - flask.request.if_modified_since and \ + flask.request.if_modified_since is not None and \ lastmodified >= flask.request.if_modified_since diff --git a/src/octoprint/server/views.py b/src/octoprint/server/views.py index dc35db57..feb6588f 100644 --- a/src/octoprint/server/views.py +++ b/src/octoprint/server/views.py @@ -37,6 +37,8 @@ _valid_div_re = re.compile("[a-zA-Z_-]+") def index(): global _templates, _plugin_names, _plugin_vars + preemptive_cache_enabled = settings().getBoolean(["devel", "cache", "preemptive"]) + # helper to check if wizards are active def wizard_active(templates): return templates is not None and bool(templates["wizard"]["order"]) @@ -116,11 +118,13 @@ def index(): files = collect_files() lastmodified = compute_lastmodified(files) lastmodified_ok = util.flask.check_lastmodified(lastmodified) - etag_ok = util.flask.check_etag(compute_etag(files, lastmodified)) + etag_ok = util.flask.check_etag(compute_etag(files=files, + lastmodified=lastmodified, + additional=cache_key())) return lastmodified_ok and etag_ok def validate_cache(cached): - etag_different = compute_etag() != cached.get_etag()[0] + etag_different = compute_etag(additional=cache_key()) != cached.get_etag()[0] return force_refresh or etag_different def collect_files(): @@ -202,8 +206,7 @@ def index(): decorated_view = util.flask.conditional(check_etag_and_lastmodified, NOT_MODIFIED)(decorated_view) return decorated_view - ui_plugins = pluginManager.get_implementations(octoprint.plugin.UiPlugin, sorting_context="UiPlugin.on_ui_render") - for plugin in ui_plugins: + def plugin_view(plugin): if plugin.will_handle_ui(request): # plugin claims responsibility, let it render the UI cached = get_cached_view(plugin._identifier, @@ -214,17 +217,18 @@ def index(): custom_etag=plugin.get_ui_custom_etag, custom_lastmodified=plugin.get_ui_custom_lastmodified) - preemptively_cached = get_preemptively_cached_view(plugin._identifier, - cached, - plugin.get_ui_data_for_preemptive_caching, - plugin.get_ui_additional_request_data_for_preemptive_caching, - plugin.get_ui_additional_unless) + if preemptive_cache_enabled and plugin.get_ui_preemptive_caching_enabled(): + view = get_preemptively_cached_view(plugin._identifier, + cached, + plugin.get_ui_data_for_preemptive_caching, + plugin.get_ui_additional_request_data_for_preemptive_caching, + plugin.get_ui_additional_unless) + else: + view = cached - response = preemptively_cached(now, request, render_kwargs) - if response is not None: - break + return view(now, request, render_kwargs) - else: + def default_view(): wizard = wizard_active(_templates) enable_accesscontrol = userManager.enabled accesscontrol_active = enable_accesscontrol and userManager.hasBeenCustomized() @@ -254,7 +258,36 @@ def index(): cached, dict(), dict()) - response = preemptively_cached() + return preemptively_cached() + + forced_view = None + preemptive_cache_environment = preemptiveCache.environment + if preemptive_cache_environment is not None and isinstance(preemptive_cache_environment, dict): + forced_view = preemptive_cache_environment.get("plugin", "_default") + + if forced_view: + # we have view forced by the preemptive cache + _logger.debug("Forcing rendering of view {}".format(forced_view)) + response = None + if forced_view != "_default": + plugin = pluginManager.get_plugin_info(forced_view, require_enabled=True) + if plugin is not None and isinstance(plugin.implementation, octoprint.plugin.UiPlugin): + response = plugin_view(plugin.implementation) + + if response is None: + return default_view() + + else: + # select view from plugins and fall back on default view if no plugin will handle it + ui_plugins = pluginManager.get_implementations(octoprint.plugin.UiPlugin, sorting_context="UiPlugin.on_ui_render") + for plugin in ui_plugins: + identifier = plugin._identifier + if plugin.will_handle_ui(request): + response = plugin_view(plugin) + if response is not None: + break + else: + response = default_view() return response