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
This commit is contained in:
Gina Häußge 2016-06-24 14:28:19 +02:00
parent 0fb1a41fef
commit 7ed71fad28
4 changed files with 87 additions and 19 deletions

View file

@ -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

View file

@ -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))

View file

@ -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

View file

@ -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