Mercurial > libervia-web
diff src/server/pages.py @ 985:64826e69f365
pages: cache mechanism, first draft:
a cache mechanism has been implemented to retrieve pages with a complexe rendering and/or calling expensive methods (e.g. network calls).
For now it's is done only for Pubsub and with service profile (i.e. profile used when user is not logged in).
When a LiberviaPage use cache, node is subscribed, and as long as no event is received (even can be item update, item retraction, or node deletion), the cached page is returned.
This is a first draft, it is planed to handle in the future logged users (which can be tricky as we must not let (un)subscribed node if user is not willing to), multi-nodes pages (e.g.: item + comments) and cache for page not depending on pubsub (e.g. chat).
author | Goffi <goffi@goffi.org> |
---|---|
date | Sun, 19 Nov 2017 17:18:14 +0100 |
parents | f0fc28b3bd1e |
children | 6daa59d44ee2 |
line wrap: on
line diff
--- a/src/server/pages.py Fri Nov 17 12:10:56 2017 +0100 +++ b/src/server/pages.py Sun Nov 19 17:18:14 2017 +0100 @@ -20,6 +20,7 @@ from twisted.web import resource as web_resource from twisted.web import util as web_util from twisted.internet import defer +from twisted.words.protocols.jabber import jid from twisted.python import failure from sat.core.i18n import _ @@ -34,6 +35,31 @@ import os.path import urllib +import time + + +class Cache(object): + + def __init__(self, rendered): + self._created = time.time() + self._last_access = self._created + self._rendered = rendered + + @property + def created(self): + return self._created + + @property + def last_access(self): + return self._last_access + + @last_access.setter + def last_access(self, timestamp): + self._last_access = timestamp + + @property + def rendered(self): + return self._rendered class LiberviaPage(web_resource.Resource): @@ -41,6 +67,10 @@ named_pages = {} uri_callbacks = {} pages_redirects = {} + cache = {} + # Set of tuples (service/node/sub_id) of nodes subscribed for caching + # sub_id can be empty string if not handled by service + cache_pubsub_sub = set() def __init__(self, host, root_dir, url, name=None, redirect=None, access=None, parse_url=None, prepare_render=None, render=None, template=None, on_data_post=None): @@ -126,6 +156,19 @@ if parse_url is not None and not callable(parse_url): log.error(_(u"parse_url must be a callable")) + # if not None, next rendering will be cached + # it must then contain a list of the the keys to use (without the page instance) + # e.g. [C.SERVICE_PROFILE, "pubsub", server@example.tld, pubsub_node] + self._do_cache = None + + def __unicode__(self): + return u'LiberviaPage {name} at {url}'.format( + name = self.name or u'<anonymous>', + url = self.url) + + def __str__(self): + return self.__unicode__.encode('utf-8') + @classmethod def importPages(cls, host, parent=None, path=None): """Recursively import Libervia pages""" @@ -324,6 +367,89 @@ request.prepath.append(pathElement) return urllib.unquote(pathElement).decode('utf-8') + def checkCacheSubscribeCb(self, sub_id, service, node): + self.cache_pubsub_sub.add((service, node, sub_id)) + + def checkCacheSubscribeEb(self, failure_, service, node): + log.warning(_(u"Can't subscribe to node: {msg}").format(msg=failure_)) + # FIXME: cache must be marked as unusable here + + def psNodeWatchAddEb(self, failure_, service, node): + log.warning(_(u"Can't add node watched: {msg}").format(msg=failure_)) + + def checkCache(self, request, cache_type, **kwargs): + """check if a page is in cache and return cached version if suitable + + this method may perform extra operation to handle cache (e.g. subscribing to a + pubsub node) + @param request(server.Request): current HTTP request + @param cache_type(int): on of C.CACHE_* const. + @param **kwargs: args according to cache_type: + C.CACHE_PROFILE: + service: pubsub service + node: pubsub node + short: short name of feature (needed if node is empty) + + """ + if request.postpath: + # we are not on the final page, no need to go further + return + profile = self.getProfile(request) or C.SERVICE_PROFILE + + if cache_type == C.CACHE_PUBSUB: + service, node = kwargs['service'], kwargs['node'] + if not node: + try: + short = kwargs['short'] + node = self.host.ns_map[short] + except KeyError: + log.warning(_(u"Can't use cache for empty node without namespace set, please ensure to set \"short\" and that it is registered")) + return + if profile != C.SERVICE_PROFILE: + # only service profile is cache for now + return + try: + cache = self.cache[profile][cache_type][service][node][self] + except KeyError: + # no cache yet, let's subscribe to the pubsub node + d1 = self.host.bridgeCall('psSubscribe', service.full(), node, {}, profile) + d1.addCallback(self.checkCacheSubscribeCb, service, node) + d1.addErrback(self.checkCacheSubscribeEb, service, node) + d2 = self.host.bridgeCall('psNodeWatchAdd', service.full(), node, profile) + d2.addErrback(self.psNodeWatchAddEb, service, node) + self._do_cache = [profile, cache_type, service, node] + # we don't return the Deferreds as it is not needed to wait for + # the subscription to continue with page rendering + return + + else: + raise exceptions.InternalError(u'Unknown cache_type') + log.debug(u'using cache for {page}'.format(page=self)) + cache.last_access = time.time() + request.write(cache.rendered) + request.finish() + raise failure.Failure(exceptions.CancelError(u'cache is used')) + + @classmethod + def onNodeEvent(cls, host, service, node, event_type, items, profile): + """Invalidate cache for all pages linked to this node""" + try: + cache = cls.cache[profile][C.CACHE_PUBSUB][jid.JID(service)][node] + except KeyError: + log.info(_(u'Removing subscription for {service}/{node}: ' + u'the page is not cached').format(service=service, node=node)) + d1 = host.bridgeCall('psUnsubscribe', service, node, profile) + d1.addErrback(lambda failure_: + log.warning(_(u"Can't unsubscribe from {service}/{node}: {msg}").format( + service=service, node=node, msg=failure_))) + d2 = host.bridgeCall('psNodeWatchAdd', service, node, profile) + # TODO: check why the page is not in cache, remove subscription? + d2.addErrback(lambda failure_: + log.warning(_(u"Can't remove watch for {service}/{node}: {msg}").format( + service=service, node=node, msg=failure_))) + else: + cache.clear() + def HTTPRedirect(self, request, url): """redirect to an URL using HTTP redirection @@ -414,8 +540,16 @@ """write data to transport and finish the request""" if data is None: self.pageError(request) - request.write(data.encode('utf-8')) + data_encoded = data.encode('utf-8') + request.write(data_encoded) request.finish() + if self._do_cache is not None: + cache = reduce(lambda d, k: d.setdefault(k, {}), self._do_cache, self.cache) + cache[self] = Cache(data_encoded) + log.debug(_(u'{page} put in cache for [{profile}]').format( + page=self, + profile=self._do_cache[0])) + self._do_cache = None def _subpagesHandler(self, dummy, request): """render subpage if suitable