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