[IMP] base, web: generate assets outside rendering

The generation inside the rendering has some drawbacks:

- `commit_assetsbundle` is needed for reports rendering because the
template rendering may generate some assets that will be accessed by
another transaction before the transaction is committed. But this
solution is not ideal since the transaction is committed in the middle
of the request

- when the first rendered page is a 404, the assets are not committed
and the page is broken.

- when starting, deleting an attachment can create a concurrent update
error and the request is retried. This will occur once per attachment
and for all worker trying to access the same resource. The whole
transaction is rollbacked, even the previously created assets bundle.

- The cold page load is a slower since there is more work to do.

- Implementing a readonly request is difficult because it could be
transformed to read write and re-executed if the assets bundle does not
exist.

Generating assets when needed solves those issues. The concurrency
when deleting an assets could still occur but only once per bundle, and
in a smaller transaction. This could be solved with a lock now that we
have more control on the transaction. The commit_assetsbundle can be
removed and 404 page should have a correct layout. The cold page load
could be a little faster because the assets bundle can be generated in
parallel requests instead of sequentially when rendering the page.

Part-of: odoo/odoo#131353
This commit is contained in:
Xavier-Do
2023-10-27 11:34:52 +00:00
parent 000842bd3d
commit 64bbef5bc1
14 changed files with 218 additions and 285 deletions
+52 -17
View File
@@ -8,6 +8,8 @@ import logging
import os
import unicodedata
import werkzeug.exceptions
try:
from werkzeug.utils import send_file
except ImportError:
@@ -15,13 +17,12 @@ except ImportError:
import odoo
import odoo.modules.registry
from odoo import http, _
from odoo import SUPERUSER_ID, _, http
from odoo.exceptions import AccessError, UserError
from odoo.http import request, Response
from odoo.tools import file_open, file_path, replace_exceptions
from odoo.tools.mimetypes import guess_mimetype
from odoo.tools.image import image_guess_size_from_field_name
from odoo.tools.mimetypes import guess_mimetype
_logger = logging.getLogger(__name__)
@@ -83,26 +84,60 @@ class Binary(http.Controller):
res.headers['Content-Security-Policy'] = "default-src 'none'"
return res
@http.route(['/web/assets/debug/<path:extra>/<string:filename>',
@http.route([
'/web/assets/<path:extra>/<string:filename>',
'/web/assets/<string:unique>/<path:extra>/<string:filename>'], type='http', auth="public")
def content_assets(self, filename=None, unique='%', extra='-', nocache=False):
url = f'/web/assets/{unique}/{extra}/{filename}'
domain = [
('public', '=', True),
('url', '!=', False),
('url', '=like', url),
('res_model', '=', 'ir.ui.view'),
('res_id', '=', 0),
]
attachment = request.env['ir.attachment'].sudo().search(domain, limit=1)
debug_assets = unique == 'debug'
attachment = None
if unique != 'debug':
url = f'/web/assets/{unique}/{extra}/{filename}'
domain = [
('public', '=', True),
('url', '!=', False),
('url', '=like', url),
('res_model', '=', 'ir.ui.view'),
('res_id', '=', 0),
('create_uid', '=', SUPERUSER_ID),
]
attachment = request.env['ir.attachment'].sudo().search(domain, limit=1)
if not attachment:
# try to generate one
with replace_exceptions(ValueError, by=werkzeug.exceptions.BadRequest):
extra_parts = extra.split('+')
params = request.env['ir.asset']._parse_assets_extra(extra_parts)
if debug_assets:
bundle_name, asset_type = filename.rsplit('.', 1)
else:
bundle_name, min_, asset_type = filename.rsplit('.', 2)
if min_ != 'min':
_logger.error("'min' expected in extension in non debug mode")
raise request.not_found()
css = asset_type == 'css'
js = asset_type == 'js'
if not js and not css:
_logger.error('Only js and css assets bundle are supported for now')
raise request.not_found()
bundle = request.env['ir.qweb']._get_asset_bundle(
bundle_name,
css=css,
js=js,
debug_assets=debug_assets,
rtl='rtl' in extra_parts,
assets_params=params,
)
# check if the version matches. If not, redirect to the last version
if not debug_assets and unique != '%' and unique != bundle.get_version(asset_type):
return request.redirect(bundle.get_link(asset_type))
if css and bundle.stylesheets:
attachment = bundle.css()
elif js and bundle.javascripts:
attachment = bundle.js()
if not attachment:
raise request.not_found()
with replace_exceptions(UserError, by=request.not_found()):
stream = request.env['ir.binary']._get_stream_from(attachment, 'raw', filename)
stream = request.env['ir.binary']._get_stream_from(attachment, 'raw', filename)
send_file_kwargs = {'as_attachment': False}
if unique:
if unique and unique != 'debug':
send_file_kwargs['immutable'] = True
send_file_kwargs['max_age'] = http.STATIC_CACHE_LONG
if nocache: