comparison 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
comparison
equal deleted inserted replaced
984:f0fc28b3bd1e 985:64826e69f365
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. 18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 from twisted.web import server 19 from twisted.web import server
20 from twisted.web import resource as web_resource 20 from twisted.web import resource as web_resource
21 from twisted.web import util as web_util 21 from twisted.web import util as web_util
22 from twisted.internet import defer 22 from twisted.internet import defer
23 from twisted.words.protocols.jabber import jid
23 from twisted.python import failure 24 from twisted.python import failure
24 25
25 from sat.core.i18n import _ 26 from sat.core.i18n import _
26 from sat.core import exceptions 27 from sat.core import exceptions
27 from sat.tools.common import uri as common_uri 28 from sat.tools.common import uri as common_uri
32 from libervia.server.utils import quote 33 from libervia.server.utils import quote
33 import libervia 34 import libervia
34 35
35 import os.path 36 import os.path
36 import urllib 37 import urllib
38 import time
39
40
41 class Cache(object):
42
43 def __init__(self, rendered):
44 self._created = time.time()
45 self._last_access = self._created
46 self._rendered = rendered
47
48 @property
49 def created(self):
50 return self._created
51
52 @property
53 def last_access(self):
54 return self._last_access
55
56 @last_access.setter
57 def last_access(self, timestamp):
58 self._last_access = timestamp
59
60 @property
61 def rendered(self):
62 return self._rendered
37 63
38 64
39 class LiberviaPage(web_resource.Resource): 65 class LiberviaPage(web_resource.Resource):
40 isLeaf = True # we handle subpages ourself 66 isLeaf = True # we handle subpages ourself
41 named_pages = {} 67 named_pages = {}
42 uri_callbacks = {} 68 uri_callbacks = {}
43 pages_redirects = {} 69 pages_redirects = {}
70 cache = {}
71 # Set of tuples (service/node/sub_id) of nodes subscribed for caching
72 # sub_id can be empty string if not handled by service
73 cache_pubsub_sub = set()
44 74
45 def __init__(self, host, root_dir, url, name=None, redirect=None, access=None, parse_url=None, 75 def __init__(self, host, root_dir, url, name=None, redirect=None, access=None, parse_url=None,
46 prepare_render=None, render=None, template=None, on_data_post=None): 76 prepare_render=None, render=None, template=None, on_data_post=None):
47 """initiate LiberviaPages 77 """initiate LiberviaPages
48 78
123 else: 153 else:
124 if render is not None: 154 if render is not None:
125 log.error(_(u"render can't be used at the same time as template")) 155 log.error(_(u"render can't be used at the same time as template"))
126 if parse_url is not None and not callable(parse_url): 156 if parse_url is not None and not callable(parse_url):
127 log.error(_(u"parse_url must be a callable")) 157 log.error(_(u"parse_url must be a callable"))
158
159 # if not None, next rendering will be cached
160 # it must then contain a list of the the keys to use (without the page instance)
161 # e.g. [C.SERVICE_PROFILE, "pubsub", server@example.tld, pubsub_node] 
162 self._do_cache = None
163
164 def __unicode__(self):
165 return u'LiberviaPage {name} at {url}'.format(
166 name = self.name or u'<anonymous>',
167 url = self.url)
168
169 def __str__(self):
170 return self.__unicode__.encode('utf-8')
128 171
129 @classmethod 172 @classmethod
130 def importPages(cls, host, parent=None, path=None): 173 def importPages(cls, host, parent=None, path=None):
131 """Recursively import Libervia pages""" 174 """Recursively import Libervia pages"""
132 if path is None: 175 if path is None:
322 """ 365 """
323 pathElement = request.postpath.pop(0) 366 pathElement = request.postpath.pop(0)
324 request.prepath.append(pathElement) 367 request.prepath.append(pathElement)
325 return urllib.unquote(pathElement).decode('utf-8') 368 return urllib.unquote(pathElement).decode('utf-8')
326 369
370 def checkCacheSubscribeCb(self, sub_id, service, node):
371 self.cache_pubsub_sub.add((service, node, sub_id))
372
373 def checkCacheSubscribeEb(self, failure_, service, node):
374 log.warning(_(u"Can't subscribe to node: {msg}").format(msg=failure_))
375 # FIXME: cache must be marked as unusable here
376
377 def psNodeWatchAddEb(self, failure_, service, node):
378 log.warning(_(u"Can't add node watched: {msg}").format(msg=failure_))
379
380 def checkCache(self, request, cache_type, **kwargs):
381 """check if a page is in cache and return cached version if suitable
382
383 this method may perform extra operation to handle cache (e.g. subscribing to a
384 pubsub node)
385 @param request(server.Request): current HTTP request
386 @param cache_type(int): on of C.CACHE_* const.
387 @param **kwargs: args according to cache_type:
388 C.CACHE_PROFILE:
389 service: pubsub service
390 node: pubsub node
391 short: short name of feature (needed if node is empty)
392
393 """
394 if request.postpath:
395 # we are not on the final page, no need to go further
396 return
397 profile = self.getProfile(request) or C.SERVICE_PROFILE
398
399 if cache_type == C.CACHE_PUBSUB:
400 service, node = kwargs['service'], kwargs['node']
401 if not node:
402 try:
403 short = kwargs['short']
404 node = self.host.ns_map[short]
405 except KeyError:
406 log.warning(_(u"Can't use cache for empty node without namespace set, please ensure to set \"short\" and that it is registered"))
407 return
408 if profile != C.SERVICE_PROFILE:
409 # only service profile is cache for now
410 return
411 try:
412 cache = self.cache[profile][cache_type][service][node][self]
413 except KeyError:
414 # no cache yet, let's subscribe to the pubsub node
415 d1 = self.host.bridgeCall('psSubscribe', service.full(), node, {}, profile)
416 d1.addCallback(self.checkCacheSubscribeCb, service, node)
417 d1.addErrback(self.checkCacheSubscribeEb, service, node)
418 d2 = self.host.bridgeCall('psNodeWatchAdd', service.full(), node, profile)
419 d2.addErrback(self.psNodeWatchAddEb, service, node)
420 self._do_cache = [profile, cache_type, service, node]
421 # we don't return the Deferreds as it is not needed to wait for
422 # the subscription to continue with page rendering
423 return
424
425 else:
426 raise exceptions.InternalError(u'Unknown cache_type')
427 log.debug(u'using cache for {page}'.format(page=self))
428 cache.last_access = time.time()
429 request.write(cache.rendered)
430 request.finish()
431 raise failure.Failure(exceptions.CancelError(u'cache is used'))
432
433 @classmethod
434 def onNodeEvent(cls, host, service, node, event_type, items, profile):
435 """Invalidate cache for all pages linked to this node"""
436 try:
437 cache = cls.cache[profile][C.CACHE_PUBSUB][jid.JID(service)][node]
438 except KeyError:
439 log.info(_(u'Removing subscription for {service}/{node}: '
440 u'the page is not cached').format(service=service, node=node))
441 d1 = host.bridgeCall('psUnsubscribe', service, node, profile)
442 d1.addErrback(lambda failure_:
443 log.warning(_(u"Can't unsubscribe from {service}/{node}: {msg}").format(
444 service=service, node=node, msg=failure_)))
445 d2 = host.bridgeCall('psNodeWatchAdd', service, node, profile)
446 # TODO: check why the page is not in cache, remove subscription?
447 d2.addErrback(lambda failure_:
448 log.warning(_(u"Can't remove watch for {service}/{node}: {msg}").format(
449 service=service, node=node, msg=failure_)))
450 else:
451 cache.clear()
452
327 def HTTPRedirect(self, request, url): 453 def HTTPRedirect(self, request, url):
328 """redirect to an URL using HTTP redirection 454 """redirect to an URL using HTTP redirection
329 455
330 @param request(server.Request): current HTTP request 456 @param request(server.Request): current HTTP request
331 @param url(unicode): url to redirect to 457 @param url(unicode): url to redirect to
412 538
413 def writeData(self, data, request): 539 def writeData(self, data, request):
414 """write data to transport and finish the request""" 540 """write data to transport and finish the request"""
415 if data is None: 541 if data is None:
416 self.pageError(request) 542 self.pageError(request)
417 request.write(data.encode('utf-8')) 543 data_encoded = data.encode('utf-8')
544 request.write(data_encoded)
418 request.finish() 545 request.finish()
546 if self._do_cache is not None:
547 cache = reduce(lambda d, k: d.setdefault(k, {}), self._do_cache, self.cache)
548 cache[self] = Cache(data_encoded)
549 log.debug(_(u'{page} put in cache for [{profile}]').format(
550 page=self,
551 profile=self._do_cache[0]))
552 self._do_cache = None
419 553
420 def _subpagesHandler(self, dummy, request): 554 def _subpagesHandler(self, dummy, request):
421 """render subpage if suitable 555 """render subpage if suitable
422 556
423 this method checks if there is still an unmanaged part of the path 557 this method checks if there is still an unmanaged part of the path