summaryrefslogtreecommitdiff
path: root/addons/website
diff options
context:
space:
mode:
authorJeremy Kersten <jke@odoo.com>2020-08-20 12:30:44 +0000
committerJeremy Kersten <jke@odoo.com>2020-08-21 17:50:00 +0000
commitcbf8d3b3047c94a2ff7fb197bbae92a402e0b58b (patch)
tree3ed943cb913cf0023a84ee22900ff7e06e8cf7a3 /addons/website
parent9b113830ea8469659a57fc04bb116e650a467285 (diff)
[IMP] website: perf - cache public page
closes odoo/odoo#56331 Signed-off-by: Jérémy Kersten (jke) <jke@openerp.com>
Diffstat (limited to 'addons/website')
-rw-r--r--addons/website/models/ir_http.py64
-rw-r--r--addons/website/models/website_page.py13
-rw-r--r--addons/website/tests/test_performance.py2
-rw-r--r--addons/website/views/website_views.xml2
4 files changed, 76 insertions, 5 deletions
diff --git a/addons/website/models/ir_http.py b/addons/website/models/ir_http.py
index d29d367e5c2..53762ac85c3 100644
--- a/addons/website/models/ir_http.py
+++ b/addons/website/models/ir_http.py
@@ -4,6 +4,7 @@ import logging
from lxml import etree
import os
import unittest
+import time
import pytz
import werkzeug
@@ -18,6 +19,7 @@ from odoo import registry, SUPERUSER_ID
from odoo.http import request
from odoo.tools.safe_eval import safe_eval
from odoo.osv.expression import FALSE_DOMAIN
+from odoo.tools.lru import LRU
from odoo.addons.http_routing.models.ir_http import ModelConverter, _guess_mimetype
from odoo.addons.portal.controllers.portal import _build_url_w_params
@@ -68,6 +70,7 @@ class Http(models.AbstractModel):
@classmethod
def clear_caches(cls):
super(Http, cls)._clear_routing_map()
+ cls.clear_cached_pages()
return super(Http, cls).clear_caches()
@classmethod
@@ -139,15 +142,23 @@ class Http(models.AbstractModel):
@classmethod
def _register_website_track(cls, response):
- if getattr(response, 'status_code', 0) != 200 or not hasattr(response, 'qcontext'):
+ if getattr(response, 'status_code', 0) != 200:
return False
- main_object = response.qcontext.get('main_object')
- website_page = getattr(main_object, '_name', False) == 'website.page' and main_object
- template = response.qcontext.get('response_template')
+
+ template = False
+ if hasattr(response, 'qcontext'): # classic response
+ main_object = response.qcontext.get('main_object')
+ website_page = getattr(main_object, '_name', False) == 'website.page' and main_object
+ template = response.qcontext.get('response_template')
+ elif hasattr(response, '_cached_page'):
+ website_page, template = response._cached_page, response._cached_template
+
view = template and request.env['website'].get_template(template)
if view and view.track:
request.env['website.visitor']._handle_webpage_dispatch(response, website_page)
+ return False
+
@classmethod
def _dispatch(cls):
"""
@@ -233,6 +244,10 @@ class Http(models.AbstractModel):
return mods + [mod for mod in installed if mod.startswith('website')]
@classmethod
+ def clear_cached_pages(cls):
+ cls._cached_pages = LRU(1024)
+
+ @classmethod
def _serve_page(cls):
req_page = request.httprequest.path
page_domain = [('url', '=', req_page)] + request.website.website_domain()
@@ -240,15 +255,54 @@ class Http(models.AbstractModel):
published_domain = page_domain
# specific page first
page = request.env['website.page'].sudo().search(published_domain, order='website_id asc', limit=1)
+
if page:
# prefetch all menus (it will prefetch website.page too)
request.website.menu_id
+
if page and (request.website.is_publisher() or page.is_visible):
+ need_to_cache = False
+ cache_key = page._get_cache_key(request)
+ if (
+ page.cache_time # cache > 0
+ and request.httprequest.method == "GET"
+ and request.env.user._is_public() # only cache for unlogged user
+ and 'nocache' not in request.params # allow bypass cache / debug
+ and len(cache_key) and cache_key[-1] is not None # nocache via expr
+ ):
+ need_to_cache = True
+ if not hasattr(cls, '_cached_pages'):
+ cls.clear_cached_pages() # init LRU
+
+ # use cached version
+ try:
+ r = cls._cached_pages[cache_key]
+ if r['time'] + page.cache_time > time.time():
+ response = werkzeug.Response(r['content'], mimetype=r['contenttype'])
+ response._cached_template = r['template']
+ response._cached_page = page
+ return response
+ else:
+ cls._cached_pages[cache_key]
+ except KeyError:
+ pass
+
_, ext = os.path.splitext(req_page)
- return request.render(page.view_id.id, {
+ response = request.render(page.view_id.id, {
'deletable': True,
'main_object': page,
}, mimetype=_guess_mimetype(ext))
+
+ if need_to_cache and response.status_code == 200:
+ r = response.render()
+ cls._cached_pages[cache_key] = {
+ 'content': r,
+ 'contenttype': response.headers['Content-Type'],
+ 'time': time.time(),
+ 'template': getattr(response, 'qcontext', {}).get('response_template')
+ }
+
+ return response
return False
@classmethod
diff --git a/addons/website/models/website_page.py b/addons/website/models/website_page.py
index 32c0cfa249f..96eed5a06ee 100644
--- a/addons/website/models/website_page.py
+++ b/addons/website/models/website_page.py
@@ -3,6 +3,7 @@
from odoo.addons.http_routing.models.ir_http import slugify
from odoo import api, fields, models
+from odoo.tools.safe_eval import safe_eval
class Page(models.Model):
@@ -21,6 +22,9 @@ class Page(models.Model):
is_homepage = fields.Boolean(compute='_compute_homepage', inverse='_set_homepage', string='Homepage')
is_visible = fields.Boolean(compute='_compute_visible', string='Is Visible')
+ cache_time = fields.Integer(default=3600, help='Time to cache the page. (0 = no cache)')
+ cache_key_expr = fields.Char(help='Expression (tuple) to evaluate the cached key. \nE.g.: "(request.params.get("currency"), )"')
+
# Page options
header_overlay = fields.Boolean()
header_color = fields.Char()
@@ -202,3 +206,12 @@ class Page(models.Model):
def get_website_meta(self):
self.ensure_one()
return self.view_id.get_website_meta()
+
+ def _get_cache_key(self, req):
+ # Always call me with super() AT THE END to have cache_key_expr appended as last element
+ # It is the only way for end user to not use cache via expr.
+ # E.g (None if 'token' in request.params else 1,) will bypass cache_time
+ cache_key = (req.website.id, req.lang, req.httprequest.path)
+ if self.cache_key_expr: # e.g. (request.session.geoip.get('country_code'),)
+ cache_key += safe_eval(self.cache_key_expr, {'request': req})
+ return cache_key
diff --git a/addons/website/tests/test_performance.py b/addons/website/tests/test_performance.py
index c6701f8aaf8..466167b436f 100644
--- a/addons/website/tests/test_performance.py
+++ b/addons/website/tests/test_performance.py
@@ -19,6 +19,8 @@ EXTRA_REQUEST = 5
class UtilPerf(HttpCase):
def _get_url_hot_query(self, url):
+ url += ('?' not in url and '?' or '') + '&nocache'
+
# ensure worker is in hot state
self.url_open(url)
self.url_open(url)
diff --git a/addons/website/views/website_views.xml b/addons/website/views/website_views.xml
index e6ad38088ae..b9e9de95c71 100644
--- a/addons/website/views/website_views.xml
+++ b/addons/website/views/website_views.xml
@@ -127,6 +127,8 @@
<field name="website_indexed"/>
<field name="is_published"/>
<field name="date_publish"/>
+ <field name="cache_time" groups="base.group_no_one"/>
+ <field name="cache_key_expr" groups="base.group_no_one"/>
</group>
</group>
<label for="menu_ids" string="Related Menu Items"/>