diff --git a/addons/auth_totp/tests/test_totp.py b/addons/auth_totp/tests/test_totp.py index d0b2b1dc34c..d287a5ef4ec 100644 --- a/addons/auth_totp/tests/test_totp.py +++ b/addons/auth_totp/tests/test_totp.py @@ -34,14 +34,14 @@ class TestTOTP(HttpCase): return totp.generate(time.time() + 30).token # because not preprocessed by ControllerType metaclass totp_hook.routing_type = 'json' - self.env['ir.http']._clear_routing_map() + self.env.registry.clear_cache('routing') # patch Home to add test endpoint Home.totp_hook = http.route('/totphook', type='json', auth='none')(totp_hook) # remove endpoint and destroy routing map @self.addCleanup def _cleanup(): del Home.totp_hook - self.env['ir.http']._clear_routing_map() + self.env.registry.clear_cache('routing') def test_totp(self): # 1. Enable 2FA diff --git a/addons/auth_totp_portal/tests/test_tour.py b/addons/auth_totp_portal/tests/test_tour.py index 242c9f0dcea..1edac06344a 100644 --- a/addons/auth_totp_portal/tests/test_tour.py +++ b/addons/auth_totp_portal/tests/test_tour.py @@ -31,12 +31,12 @@ class TestTOTPortal(HttpCase): totp_hook.routing_type = 'json' # patch Home to add test endpoint Home.totp_hook = http.route('/totphook', type='json', auth='none')(totp_hook) - self.env['ir.http']._clear_routing_map() + self.env.registry.clear_cache('routing') # remove endpoint and destroy routing map @self.addCleanup def _cleanup(): del Home.totp_hook - self.env['ir.http']._clear_routing_map() + self.env.registry.clear_cache('routing') self.start_tour('/my/security', 'totportal_tour_setup', login='portal') # also disables totp otherwise we can't re-login diff --git a/addons/base_import/models/base_import.py b/addons/base_import/models/base_import.py index 1dce1e4d36e..cd0d3cf6191 100644 --- a/addons/base_import/models/base_import.py +++ b/addons/base_import/models/base_import.py @@ -1339,7 +1339,9 @@ class Import(models.TransientModel): if dryrun: self._cr.execute('ROLLBACK TO SAVEPOINT import') # cancel all changes done to the registry/ormcache - self.pool.clear_caches() + # we need to clear the cache in case any created id was added to an ormcache and would be missing afterward + self.pool.clear_all_caches() + # don't propagate to other workers since it was rollbacked self.pool.reset_changes() else: self._cr.execute('RELEASE SAVEPOINT import') diff --git a/addons/bus/tests/test_assetsbundle.py b/addons/bus/tests/test_assetsbundle.py index d520c1b4aff..e5a246f0cee 100644 --- a/addons/bus/tests/test_assetsbundle.py +++ b/addons/bus/tests/test_assetsbundle.py @@ -25,7 +25,7 @@ class BusWebTests(odoo.tests.HttpCase): ]) # start from a clean slate self.env['ir.attachment'].search(domain).unlink() - self.env.registry._clear_cache() + self.env.registry.clear_cache() sendones = [] def patched_sendone(self, channel, notificationType, message): diff --git a/addons/google_calendar/models/google_sync.py b/addons/google_calendar/models/google_sync.py index 846dd144069..49f5e0edeb4 100644 --- a/addons/google_calendar/models/google_sync.py +++ b/addons/google_calendar/models/google_sync.py @@ -64,7 +64,7 @@ class GoogleSync(models.AbstractModel): def write(self, vals): google_service = GoogleCalendarService(self.env['google.service']) if 'google_id' in vals: - self._event_ids_from_google_ids.clear_cache(self) + self.env.registry.clear_cache() # _event_ids_from_google_ids synced_fields = self._get_google_synced_fields() if 'need_sync' not in vals and vals.keys() & synced_fields and not self.env.user.google_synchronization_stopped: vals['need_sync'] = True @@ -79,7 +79,7 @@ class GoogleSync(models.AbstractModel): @api.model_create_multi def create(self, vals_list): if any(vals.get('google_id') for vals in vals_list): - self._event_ids_from_google_ids.clear_cache(self) + self.env.registry.clear_cache() # _event_ids_from_google_ids if self.env.user.google_synchronization_stopped: for vals in vals_list: vals.update({'need_sync': False}) diff --git a/addons/hr_timesheet/tests/test_performance.py b/addons/hr_timesheet/tests/test_performance.py index bd128cf6551..99c5572d327 100644 --- a/addons/hr_timesheet/tests/test_performance.py +++ b/addons/hr_timesheet/tests/test_performance.py @@ -12,8 +12,7 @@ class TestPerformanceTimesheet(TestCommonTimesheet): 'project_id': project.id, } for i in range(17) for project in projects]) self.env.invalidate_all() - projects.clear_caches() - tasks.clear_caches() + self.env.registry.clear_cache() with self.assertQueryCount(7): self.env['account.analytic.line']._timesheet_preprocess([ {'task_id': task.id} for task in tasks for _i in range(10) diff --git a/addons/http_routing/models/ir_http.py b/addons/http_routing/models/ir_http.py index 4b8f9a8c62a..63ae8c68fbd 100644 --- a/addons/http_routing/models/ir_http.py +++ b/addons/http_routing/models/ir_http.py @@ -185,15 +185,15 @@ def url_for(url_from, lang_code=None, no_rewrite=False): :param no_rewrite: don't try to match route with website.rewrite. ''' new_url = False - + rewrite = not no_rewrite # don't try to match route if we know that no rewrite has been loaded. routing = getattr(request, 'website_routing', None) # not modular, but not overridable - if not getattr(request.env['ir.http'], '_rewrite_len', {}).get(routing): - no_rewrite = True + if not request.env['ir.http']._rewrite_len(routing): + rewrite = False path, _, qs = (url_from or '').partition('?') - if (not no_rewrite and path and ( + if (rewrite and path and ( len(path) > 1 and path.startswith('/') and '/static/' not in path @@ -660,7 +660,7 @@ class IrHttp(models.AbstractModel): return response @api.model - @tools.ormcache('path', 'query_args') + @tools.ormcache('path', 'query_args', cache='routing.rewrites') def url_rewrite(self, path, query_args=None): new_url = False router = http.root.get_db_router(request.db).bind('') @@ -676,3 +676,6 @@ class IrHttp(models.AbstractModel): except werkzeug.exceptions.NotFound: new_url = path return new_url or path, endpoint and endpoint[0] + + def _rewrite_len(self, website_id, rewrites=None): + return 0 diff --git a/addons/mail/models/mail_message_subtype.py b/addons/mail/models/mail_message_subtype.py index 1715d1e9ec7..f75ab4f94d6 100644 --- a/addons/mail/models/mail_message_subtype.py +++ b/addons/mail/models/mail_message_subtype.py @@ -45,15 +45,15 @@ class MailMessageSubtype(models.Model): @api.model_create_multi def create(self, vals_list): - self.clear_caches() + self.env.registry.clear_cache() # _get_auto_subscription_subtypes return super(MailMessageSubtype, self).create(vals_list) def write(self, vals): - self.clear_caches() + self.env.registry.clear_cache() # _get_auto_subscription_subtypes return super(MailMessageSubtype, self).write(vals) def unlink(self): - self.clear_caches() + self.env.registry.clear_cache() # _get_auto_subscription_subtypes return super(MailMessageSubtype, self).unlink() @tools.ormcache('model_name') diff --git a/addons/product/models/product_product.py b/addons/product/models/product_product.py index 9a952b3cce7..a65385b74e5 100644 --- a/addons/product/models/product_product.py +++ b/addons/product/models/product_product.py @@ -314,7 +314,7 @@ class ProductProduct(models.Model): self.product_tmpl_id._sanitize_vals(vals) products = super(ProductProduct, self.with_context(create_product_product=True)).create(vals_list) # `_get_variant_id_for_combination` depends on existing variants - self.clear_caches() + self.env.registry.clear_cache() return products def write(self, values): @@ -322,10 +322,10 @@ class ProductProduct(models.Model): res = super(ProductProduct, self).write(values) if 'product_template_attribute_value_ids' in values: # `_get_variant_id_for_combination` depends on `product_template_attribute_value_ids` - self.clear_caches() + self.env.registry.clear_cache() elif 'active' in values: # `_get_first_possible_variant_id` depends on variants active state - self.clear_caches() + self.env.registry.clear_cache() return res def unlink(self): @@ -350,7 +350,7 @@ class ProductProduct(models.Model): # products due to ondelete='cascade' unlink_templates.unlink() # `_get_variant_id_for_combination` depends on existing variants - self.clear_caches() + self.env.registry.clear_cache() return res def _filter_to_unlink(self, check_access=True): diff --git a/addons/test_mail/tests/test_message_track.py b/addons/test_mail/tests/test_message_track.py index 02716f5a94e..ee72f8b628d 100644 --- a/addons/test_mail/tests/test_message_track.py +++ b/addons/test_mail/tests/test_message_track.py @@ -506,7 +506,7 @@ class TestTrackingInternals(MailCommon): # test editing the record with user not in the group of the field self.env.invalidate_all() - self.record.clear_caches() + self.env.registry.clear_cache() record_form = Form(self.record.with_user(self.user_employee)) record_form.name = 'TestDoNoCrash' # the employee user must be able to save the fields on which they can write diff --git a/addons/web/controllers/home.py b/addons/web/controllers/home.py index c02e1313e4e..f82bc1e0678 100644 --- a/addons/web/controllers/home.py +++ b/addons/web/controllers/home.py @@ -141,7 +141,7 @@ class Home(http.Controller): if request.env.user._is_system(): uid = request.session.uid = odoo.SUPERUSER_ID # invalidate session token cache as we've changed the uid - request.env['res.users'].clear_caches() + request.env.registry.clear_cache() request.session.session_token = security.compute_session_token(request.session, request.env) return request.redirect(self._login_redirect(uid)) diff --git a/addons/web/tests/test_assets.py b/addons/web/tests/test_assets.py index dd2c4349148..a1dd4d37977 100644 --- a/addons/web/tests/test_assets.py +++ b/addons/web/tests/test_assets.py @@ -69,7 +69,7 @@ class TestPregenerateTime(HttpCase): def test_logs_pregenerate_time(self): self.env['ir.qweb']._pregenerate_assets_bundles() start = time.time() - self.env.registry.clear_caches() + self.env.registry.clear_cache() self.env.cache.invalidate() with self.profile(collectors=['sql', odoo.tools.profiler.PeriodicCollector(interval=0.01)], disable_gc=True): self.env['ir.qweb']._pregenerate_assets_bundles() diff --git a/addons/web_editor/controllers/main.py b/addons/web_editor/controllers/main.py index a314bf883cf..1c78c74a982 100644 --- a/addons/web_editor/controllers/main.py +++ b/addons/web_editor/controllers/main.py @@ -629,7 +629,7 @@ class Web_Editor(http.Controller): if re.match('^o-color-([1-5])$', css_color_value): if not bundle_css: bundle = 'web.assets_frontend' - asset = request.env["ir.asset"]._get_asset_bundle(bundle) + asset = request.env["ir.qweb"]._get_asset_bundle(bundle) bundle_css = asset.css().index_content color_search = re.search(r'(?i)--%s:\s+(%s|%s)' % (css_color_value, regex_hex, regex_rgba), bundle_css) if not color_search: diff --git a/addons/web_editor/models/assets.py b/addons/web_editor/models/assets.py index e807fa30130..fd418b94b37 100644 --- a/addons/web_editor/models/assets.py +++ b/addons/web_editor/models/assets.py @@ -62,6 +62,7 @@ class Assets(models.AbstractModel): # If it was already modified, simply override the corresponding # attachment content custom_attachment.write({"datas": datas}) + self.env.registry.clear_cache('assets') else: # If not, create a new attachment to copy the original scss/js file # content, with its modifications @@ -93,7 +94,6 @@ class Assets(models.AbstractModel): new_asset['bundle'] = IrAsset._get_related_bundle(url, bundle) IrAsset.create(new_asset) - self.env["ir.qweb"].clear_caches() @api.model def _get_content_from_url(self, url, url_info=None, custom_attachments=None): diff --git a/addons/website/models/ir_http.py b/addons/website/models/ir_http.py index 45443ce5840..c019b88aba5 100644 --- a/addons/website/models/ir_http.py +++ b/addons/website/models/ir_http.py @@ -13,7 +13,7 @@ import werkzeug.utils from functools import partial import odoo -from odoo import api, models +from odoo import api, models, tools from odoo import SUPERUSER_ID from odoo.exceptions import AccessError from odoo.http import request @@ -62,16 +62,10 @@ def get_request_website(): class Http(models.AbstractModel): _inherit = 'ir.http' - @classmethod - def routing_map(cls, key=None): + def routing_map(self, key=None): if not key and request: key = request.website_routing - return super(Http, cls).routing_map(key=key) - - @classmethod - def clear_caches(cls): - super()._clear_routing_map() - return super().clear_caches() + return super().routing_map(key=key) @classmethod def _slug_matching(cls, adapter, endpoint, **kw): @@ -81,18 +75,31 @@ class Http(models.AbstractModel): qs = request.httprequest.query_string.decode('utf-8') return adapter.build(endpoint, kw) + (qs and '?%s' % qs or '') - @classmethod - def _generate_routing_rules(cls, modules, converters): + @tools.ormcache('website_id', cache='routing') + def _rewrite_len(self, website_id, rewrites=None): + # This little hack allow to store a _rewrite_len in cache if we have the value, + # (Populated during routing map computation) + # but also ensures that we will have the correct value if the cache is not populated. + # The cache key actually does not depends on "rewrites" because this is the information + # we are storing, rewrites actually depends on website_id + if rewrites is None: + rewrites = self._get_rewrites(website_id) + return len(rewrites) + + def _get_rewrites(self, website_id): + domain = [('redirect_type', 'in', ('308', '404')), '|', ('website_id', '=', False), ('website_id', '=', website_id)] + rewrites = {x.url_from: x for x in self.env['website.rewrite'].sudo().search(domain)} + self._rewrite_len(website_id, rewrites) # optionnal, update value in cache + #self._rewrite_len.add_cache_value(website_id, value=rewrites) + return rewrites + + def _generate_routing_rules(self, modules, converters): if not request: yield from super()._generate_routing_rules(modules, converters) return - website_id = request.website_routing logger.debug("_generate_routing_rules for website: %s", website_id) - domain = [('redirect_type', 'in', ('308', '404')), '|', ('website_id', '=', False), ('website_id', '=', website_id)] - - rewrites = dict([(x.url_from, x) for x in request.env['website.rewrite'].sudo().search(domain)]) - cls._rewrite_len[website_id] = len(rewrites) + rewrites = self._get_rewrites(website_id) for url, endpoint in super()._generate_routing_rules(modules, converters): if url in rewrites: @@ -104,7 +111,7 @@ class Http(models.AbstractModel): if url != url_to: logger.debug('Redirect from %s to %s for website %s' % (url, url_to, website_id)) - _slug_matching = partial(cls._slug_matching, endpoint=endpoint) + _slug_matching = partial(self._slug_matching, endpoint=endpoint) endpoint.routing['redirect_to'] = _slug_matching yield url, endpoint # yield original redirected to new url elif rewrite.redirect_type == '404': diff --git a/addons/website/models/ir_ui_view.py b/addons/website/models/ir_ui_view.py index faa383ea9f9..949e6d8b304 100644 --- a/addons/website/models/ir_ui_view.py +++ b/addons/website/models/ir_ui_view.py @@ -224,7 +224,7 @@ class View(models.Model): specific_views += view._get_specific_views() result = super(View, self + specific_views).unlink() - self.clear_caches() + self.env.registry.clear_cache('templates') return result def _create_website_specific_pages_for_view(self, new_view, website): @@ -363,7 +363,7 @@ class View(models.Model): """ @api.model - @tools.ormcache_context('self.env.uid', 'self.env.su', 'xml_id', keys=('website_id',)) + @tools.ormcache('self.env.uid', 'self.env.su', 'xml_id', 'self._context.get("website_id")', cache='templates') def _get_view_id(self, xml_id): """If a website_id is in the context and the given xml_id is not an int then try to get the id of the specific view for that website, but @@ -374,7 +374,7 @@ class View(models.Model): method. `viewref` is probably more suitable. Archived views are ignored (unless the active_test context is set, but - then the ormcache_context will not work as expected). + then the ormcache will not work as expected). """ website_id = self._context.get('website_id') if website_id and not isinstance(xml_id, int): @@ -388,7 +388,7 @@ class View(models.Model): return view.id return super(View, self.sudo())._get_view_id(xml_id) - @tools.ormcache('self.id') + @tools.ormcache('self.id', cache='templates') def _get_cached_visibility(self): return self.visibility diff --git a/addons/website/models/website.py b/addons/website/models/website.py index 7f268f7d004..927160afdcb 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -186,7 +186,7 @@ class Website(models.Model): website.menu_id = top_menus and top_menus[0].id or False # self.env.uid for ir.rule groups on menu - @tools.ormcache('self.env.uid', 'self.id') + @tools.ormcache('self.env.uid', 'self.id', cache='templates') def _get_menu_ids(self): return self.env['website.menu'].search([('website_id', '=', self.id)]).ids @@ -216,7 +216,7 @@ class Website(models.Model): original_company = self.company_id self._handle_create_write(values) - self.clear_caches() + self.env.registry.clear_cache() if 'company_id' in values and 'user_id' not in values: public_user_to_change_websites = self.filtered(lambda w: w.sudo().user_id.company_id.id != values['company_id']) @@ -228,7 +228,7 @@ class Website(models.Model): if 'cdn_activated' in values or 'cdn_url' in values or 'cdn_filters' in values: # invalidate the caches from static node at compile time - self.env['ir.qweb'].clear_caches() + self.env.registry.clear_cache() # invalidate cache for `company.website_id` to be recomputed if 'sequence' in values or 'company_id' in values: @@ -1088,7 +1088,7 @@ class Website(models.Model): return view @api.model - @tools.ormcache_context('key', keys=('website_id',)) + @tools.ormcache('key', 'self._context.get("website_id")', cache='templates') def is_view_active(self, key): """ Return True if active, False if not active, None if not found diff --git a/addons/website/models/website_menu.py b/addons/website/models/website_menu.py index 66106ae6d38..45aeaa032d0 100644 --- a/addons/website/models/website_menu.py +++ b/addons/website/models/website_menu.py @@ -73,7 +73,7 @@ class Menu(models.Model): Be careful to return correct record for ir.model.data xml_id in case of default main menus creation. ''' - self.clear_caches() + self.env.registry.clear_cache('templates') # Only used when creating website_data.xml default menu menus = self.env['website.menu'] for vals in vals_list: @@ -103,11 +103,11 @@ class Menu(models.Model): return menus def write(self, values): - self.clear_caches() + self.env.registry.clear_cache('templates') return super().write(values) def unlink(self): - self.clear_caches() + self.env.registry.clear_cache('templates') default_menu = self.env.ref('website.main_menu', raise_if_not_found=False) menus_to_remove = self for menu in self.filtered(lambda m: default_menu and m.parent_id.id == default_menu.id): diff --git a/addons/website/models/website_page.py b/addons/website/models/website_page.py index 8dc35b53a36..71c2344a48a 100644 --- a/addons/website/models/website_page.py +++ b/addons/website/models/website_page.py @@ -148,7 +148,7 @@ class Page(models.Model): # If there is no other pages linked to that ir_ui_view, we can delete the ir_ui_view page.view_id.unlink() # Make sure website._get_menu_ids() will be recomputed - self.clear_caches() + self.env.registry.clear_cache() return super(Page, self).unlink() def write(self, vals): @@ -186,7 +186,7 @@ class Page(models.Model): if 'visibility' in vals: if vals['visibility'] != 'restricted_group': vals['groups_id'] = False - self.clear_caches() # write on page == write on view that invalid cache + self.env.registry.clear_cache() # write on page == write on view that invalid cache return super(Page, self).write(vals) def get_website_meta(self): diff --git a/addons/website/models/website_rewrite.py b/addons/website/models/website_rewrite.py index f4be519514a..f21439dc2a1 100644 --- a/addons/website/models/website_rewrite.py +++ b/addons/website/models/website_rewrite.py @@ -131,12 +131,12 @@ class WebsiteRewrite(models.Model): return res def _invalidate_routing(self): - # Call clear_caches on this worker to reload routing table. + # Call clear_cache for routing on all workers to reload routing table. # Note that only 404 and 308 redirection alter the routing map: # - 404: remove entry from routing map # - 301/302: served as fallback later if path not found in routing map # - 308: add "alias" (`redirect_to`) in routing map - self.env['ir.http'].clear_caches() + self.env.registry.clear_cache('routing') def refresh_routes(self): self.env['website.route']._refresh() diff --git a/addons/website/tests/test_http_endpoint.py b/addons/website/tests/test_http_endpoint.py index f3845953cf1..d5082eca8e2 100644 --- a/addons/website/tests/test_http_endpoint.py +++ b/addons/website/tests/test_http_endpoint.py @@ -25,7 +25,7 @@ class TestHttpEndPoint(HttpCase): 'inherit_id': homepage_view.id, 'arch_db': """ - + """, }) diff --git a/addons/website/tests/test_views.py b/addons/website/tests/test_views.py index a09c74a77d7..18768afd9bf 100644 --- a/addons/website/tests/test_views.py +++ b/addons/website/tests/test_views.py @@ -1163,7 +1163,7 @@ class TestCowViewSaving(TestViewSavingCommon): french = self.env['res.lang']._activate_lang('fr_FR') self.env['ir.module.module']._load_module_terms(['website'], [french.code]) # Make sure res.lang.get_installed is recomputed - self.env.registry.clear_caches() + self.env.registry.clear_cache() View = self.env['ir.ui.view'].with_context(lang=french.code, website_id=1) old_specific_views = View.search([('website_id', '!=', None)]) diff --git a/addons/website_crm_iap_reveal/models/crm_reveal_rule.py b/addons/website_crm_iap_reveal/models/crm_reveal_rule.py index 732b56fbbbc..bee08966024 100644 --- a/addons/website_crm_iap_reveal/models/crm_reveal_rule.py +++ b/addons/website_crm_iap_reveal/models/crm_reveal_rule.py @@ -85,7 +85,7 @@ class CRMRevealRule(models.Model): @api.model_create_multi def create(self, vals_list): - self.clear_caches() # Clear the cache in order to recompute _get_active_rules + self.env.registry.clear_cache() # Clear the cache in order to recompute _get_active_rules return super().create(vals_list) def write(self, vals): @@ -93,11 +93,11 @@ class CRMRevealRule(models.Model): 'country_ids', 'regex_url', 'active' } if set(vals.keys()) & fields_set: - self.clear_caches() # Clear the cache in order to recompute _get_active_rules + self.env.registry.clear_cache() # Clear the cache in order to recompute _get_active_rules return super(CRMRevealRule, self).write(vals) def unlink(self): - self.clear_caches() # Clear the cache in order to recompute _get_active_rules + self.env.registry.clear_cache() # Clear the cache in order to recompute _get_active_rules return super(CRMRevealRule, self).unlink() def action_get_lead_tree_view(self): diff --git a/addons/website_sale/models/product_pricelist.py b/addons/website_sale/models/product_pricelist.py index 1b3e7483c30..eb088aab077 100644 --- a/addons/website_sale/models/product_pricelist.py +++ b/addons/website_sale/models/product_pricelist.py @@ -41,17 +41,18 @@ class ProductPricelist(models.Model): # It be set when we actually create the pricelist self = self.with_context(default_company_id=vals['company_id']) pricelists = super().create(vals_list) - pricelists and pricelists.clear_caches() + if pricelists: + self.env.registry.clear_cache() return pricelists def write(self, data): res = super(ProductPricelist, self).write(data) - self and self.clear_caches() + self and self.env.registry.clear_cache() return res def unlink(self): res = super(ProductPricelist, self).unlink() - self and self.clear_caches() + self and self.env.registry.clear_cache() return res def _get_partner_pricelist_multi_search_domain_hook(self, company_id): diff --git a/odoo/addons/base/models/assetsbundle.py b/odoo/addons/base/models/assetsbundle.py index bfda143199c..fe631138734 100644 --- a/odoo/addons/base/models/assetsbundle.py +++ b/odoo/addons/base/models/assetsbundle.py @@ -302,7 +302,7 @@ class AssetsBundle(object): but the transaction is rollbacked, leading to 404 when getting the attachments. This postrollback hook will help fix this issue by clearing the cache if it isn't commited. """ - self.env.cr.postrollback.add(self.env.registry._Registry__cache.clear) + self.env.cr.postrollback.add(self.env.registry._Registry__caches['assets'].clear) def save_attachment(self, extension, content): """Record the given bundle in an ir.attachment and delete diff --git a/odoo/addons/base/models/decimal_precision.py b/odoo/addons/base/models/decimal_precision.py index 3d01a676f89..373099ebaf7 100644 --- a/odoo/addons/base/models/decimal_precision.py +++ b/odoo/addons/base/models/decimal_precision.py @@ -36,17 +36,17 @@ class DecimalPrecision(models.Model): @api.model_create_multi def create(self, vals_list): res = super(DecimalPrecision, self).create(vals_list) - self.clear_caches() + self.env.registry.clear_cache() return res def write(self, data): res = super(DecimalPrecision, self).write(data) - self.clear_caches() + self.env.registry.clear_cache() return res def unlink(self): res = super(DecimalPrecision, self).unlink() - self.clear_caches() + self.env.registry.clear_cache() return res @api.onchange('digits') diff --git a/odoo/addons/base/models/ir_actions.py b/odoo/addons/base/models/ir_actions.py index 9e375a5237d..34d2ad39293 100644 --- a/odoo/addons/base/models/ir_actions.py +++ b/odoo/addons/base/models/ir_actions.py @@ -73,13 +73,13 @@ class IrActions(models.Model): def create(self, vals_list): res = super(IrActions, self).create(vals_list) # self.get_bindings() depends on action records - self.clear_caches() + self.env.registry.clear_cache() return res def write(self, vals): res = super(IrActions, self).write(vals) # self.get_bindings() depends on action records - self.clear_caches() + self.env.registry.clear_cache() return res def unlink(self): @@ -89,7 +89,7 @@ class IrActions(models.Model): todos.unlink() res = super(IrActions, self).unlink() # self.get_bindings() depends on action records - self.clear_caches() + self.env.registry.clear_cache() return res @api.ondelete(at_uninstall=True) @@ -303,14 +303,14 @@ class IrActionsActWindow(models.Model): @api.model_create_multi def create(self, vals_list): - self.clear_caches() + self.env.registry.clear_cache() for vals in vals_list: if not vals.get('name') and vals.get('res_model'): vals['name'] = self.env[vals['res_model']]._description return super(IrActionsActWindow, self).create(vals_list) def unlink(self): - self.clear_caches() + self.env.registry.clear_cache() return super(IrActionsActWindow, self).unlink() def exists(self): diff --git a/odoo/addons/base/models/ir_asset.py b/odoo/addons/base/models/ir_asset.py index deaa7711c5a..d9b8d9e9a02 100644 --- a/odoo/addons/base/models/ir_asset.py +++ b/odoo/addons/base/models/ir_asset.py @@ -64,15 +64,15 @@ class IrAsset(models.Model): @api.model_create_multi def create(self, vals_list): - self.clear_caches() + self.env.registry.clear_cache('assets') return super().create(vals_list) def write(self, values): - self.clear_caches() + self.env.registry.clear_cache('assets') return super().write(values) def unlink(self): - self.clear_caches() + self.env.registry.clear_cache('assets') return super().unlink() name = fields.Char(string='Name', required=True) @@ -106,7 +106,7 @@ class IrAsset(models.Model): @tools.conditional( 'xml' not in tools.config['dev_mode'], - tools.ormcache('bundle', 'tuple(sorted(assets_params.items()))'), + tools.ormcache('bundle', 'tuple(sorted(assets_params.items()))', cache='assets'), ) def _get_asset_paths(self, bundle, assets_params): """ diff --git a/odoo/addons/base/models/ir_config_parameter.py b/odoo/addons/base/models/ir_config_parameter.py index db8f38a8b35..a5654a99296 100644 --- a/odoo/addons/base/models/ir_config_parameter.py +++ b/odoo/addons/base/models/ir_config_parameter.py @@ -102,13 +102,13 @@ class IrConfigParameter(models.Model): @api.model_create_multi def create(self, vals_list): - self.clear_caches() + self.env.registry.clear_cache() return super(IrConfigParameter, self).create(vals_list) def write(self, vals): - self.clear_caches() + self.env.registry.clear_cache() return super(IrConfigParameter, self).write(vals) def unlink(self): - self.clear_caches() + self.env.registry.clear_cache() return super(IrConfigParameter, self).unlink() diff --git a/odoo/addons/base/models/ir_default.py b/odoo/addons/base/models/ir_default.py index 3c5e6fe36bd..b28c5b62a60 100644 --- a/odoo/addons/base/models/ir_default.py +++ b/odoo/addons/base/models/ir_default.py @@ -32,17 +32,17 @@ class IrDefault(models.Model): @api.model_create_multi def create(self, vals_list): - self.clear_caches() + self.env.registry.clear_cache() return super(IrDefault, self).create(vals_list) def write(self, vals): if self: - self.clear_caches() + self.env.registry.clear_cache() return super(IrDefault, self).write(vals) def unlink(self): if self: - self.clear_caches() + self.env.registry.clear_cache() return super(IrDefault, self).unlink() @api.model diff --git a/odoo/addons/base/models/ir_http.py b/odoo/addons/base/models/ir_http.py index 96706f90ab3..d0a9706721e 100644 --- a/odoo/addons/base/models/ir_http.py +++ b/odoo/addons/base/models/ir_http.py @@ -128,8 +128,8 @@ class IrHttp(models.AbstractModel): return {'model': ModelConverter, 'models': ModelsConverter, 'int': SignedIntConverter} @classmethod - def _match(cls, path_info, key=None): - rule, args = cls.routing_map().bind_to_environ(request.httprequest.environ).match(path_info=path_info, return_rule=True) + def _match(cls, path_info): + rule, args = request.env['ir.http'].routing_map().bind_to_environ(request.httprequest.environ).match(path_info=path_info, return_rule=True) return rule, args @classmethod @@ -216,44 +216,30 @@ class IrHttp(models.AbstractModel): def _redirect(cls, location, code=303): return werkzeug.utils.redirect(location, code=code, Response=Response) - @classmethod - def _generate_routing_rules(cls, modules, converters): + def _generate_routing_rules(self, modules, converters): return http._generate_routing_rules(modules, False, converters) - @classmethod - def routing_map(cls, key=None): - - if not hasattr(cls, '_routing_map'): - cls._routing_map = {} - cls._rewrite_len = {} - - if key not in cls._routing_map: - _logger.info("Generating routing map for key %s" % str(key)) - registry = Registry(threading.current_thread().dbname) - installed = registry._init_modules.union(odoo.conf.server_wide_modules) - if tools.config['test_enable'] and odoo.modules.module.current_test: - installed.add(odoo.modules.module.current_test) - mods = sorted(installed) - # Note : when routing map is generated, we put it on the class `cls` - # to make it available for all instance. Since `env` create an new instance - # of the model, each instance will regenared its own routing map and thus - # regenerate its EndPoint. The routing map should be static. - routing_map = werkzeug.routing.Map(strict_slashes=False, converters=cls._get_converters()) - for url, endpoint in cls._generate_routing_rules(mods, converters=cls._get_converters()): - routing = submap(endpoint.routing, ROUTING_KEYS) - if routing['methods'] is not None and 'OPTIONS' not in routing['methods']: - routing['methods'] = routing['methods'] + ['OPTIONS'] - rule = FasterRule(url, endpoint=endpoint, **routing) - rule.merge_slashes = False - routing_map.add(rule) - cls._routing_map[key] = routing_map - return cls._routing_map[key] - - @classmethod - def _clear_routing_map(cls): - if hasattr(cls, '_routing_map'): - cls._routing_map = {} - _logger.debug("Clear routing map") + @tools.ormcache('key', cache='routing') + def routing_map(self, key=None): + _logger.info("Generating routing map for key %s", str(key)) + registry = Registry(threading.current_thread().dbname) + installed = registry._init_modules.union(odoo.conf.server_wide_modules) + if tools.config['test_enable'] and odoo.modules.module.current_test: + installed.add(odoo.modules.module.current_test) + mods = sorted(installed) + # Note : when routing map is generated, we put it on the class `cls` + # to make it available for all instance. Since `env` create an new instance + # of the model, each instance will regenared its own routing map and thus + # regenerate its EndPoint. The routing map should be static. + routing_map = werkzeug.routing.Map(strict_slashes=False, converters=self._get_converters()) + for url, endpoint in self._generate_routing_rules(mods, converters=self._get_converters()): + routing = submap(endpoint.routing, ROUTING_KEYS) + if routing['methods'] is not None and 'OPTIONS' not in routing['methods']: + routing['methods'] = routing['methods'] + ['OPTIONS'] + rule = FasterRule(url, endpoint=endpoint, **routing) + rule.merge_slashes = False + routing_map.add(rule) + return routing_map @api.autovacuum def _gc_sessions(self): diff --git a/odoo/addons/base/models/ir_model.py b/odoo/addons/base/models/ir_model.py index 6e54dbbc01a..1e6e37c0d7f 100644 --- a/odoo/addons/base/models/ir_model.py +++ b/odoo/addons/base/models/ir_model.py @@ -932,7 +932,7 @@ class IrModelFields(models.Model): models.add(vals['model']) # for self._get_ids() in _update_selection() - self.clear_caches() + self.env.registry.clear_cache() res = super(IrModelFields, self).create(vals_list) @@ -1042,7 +1042,7 @@ class IrModelFields(models.Model): def update_field_translations(self, field_name, translations): res = super().update_field_translations(field_name, translations) if res: - self.clear_caches() + self.env.registry.clear_cache() return res @api.depends('field_description', 'model') @@ -1917,23 +1917,11 @@ class IrModelAccess(models.Model): return has_access - __cache_clearing_methods = set() - - @classmethod - def register_cache_clearing_method(cls, model, method): - cls.__cache_clearing_methods.add((model, method)) - - @classmethod - def unregister_cache_clearing_method(cls, model, method): - cls.__cache_clearing_methods.discard((model, method)) @api.model def call_cache_clearing_methods(self): self.env.invalidate_all() - self._get_allowed_models.clear_cache(self) # clear the cache of check function - for model, method in self.__cache_clearing_methods: - if model in self.env: - getattr(self.env[model], method)() + self.env.registry.clear_cache() # mainly _get_allowed_models # # Check rights on actions @@ -2019,7 +2007,7 @@ class IrModelData(models.Model): # NEW V8 API @api.model - @tools.ormcache('xmlid') + @tools.ormcache('xmlid', cache='xmlid') def _xmlid_lookup(self, xmlid: str) -> tuple: """Low level xmlid lookup Return (id, res_model, res_id) or raise ValueError if not found @@ -2066,9 +2054,13 @@ class IrModelData(models.Model): default = dict(default or {}, name="%s_%s" % (self.name, rand)) return super().copy(default) + def write(self, values): + self.env.registry.clear_cache('xmlid') # _xmlid_lookup + return super().write(values) + def unlink(self): """ Regular unlink method, but make sure to clear the caches. """ - self.clear_caches() + self.env.registry.clear_cache('xmlid') # _xmlid_lookup return super(IrModelData, self).unlink() def _lookup_xmlids(self, xml_ids, model): @@ -2120,6 +2112,7 @@ class IrModelData(models.Model): query = self._build_update_xmlids_query(sub_rows, update) try: self.env.cr.execute(query, [arg for row in sub_rows for arg in row]) + self.env.registry.clear_cache('xmlid') except Exception: _logger.error("Failed to insert ir_model_data\n%s", "\n".join(str(row) for row in sub_rows)) raise diff --git a/odoo/addons/base/models/ir_module.py b/odoo/addons/base/models/ir_module.py index 6f304e70c77..0442554cf10 100644 --- a/odoo/addons/base/models/ir_module.py +++ b/odoo/addons/base/models/ir_module.py @@ -311,7 +311,7 @@ class Module(models.Model): raise UserError(_('You are trying to remove a module that is installed or will be installed.')) def unlink(self): - self.clear_caches() + self.env.registry.clear_cache() return super(Module, self).unlink() def _get_modules_to_load_domain(self): diff --git a/odoo/addons/base/models/ir_property.py b/odoo/addons/base/models/ir_property.py index 8a7d4fd6f83..86c53f47272 100644 --- a/odoo/addons/base/models/ir_property.py +++ b/odoo/addons/base/models/ir_property.py @@ -128,9 +128,9 @@ class Property(models.Model): # DLE P44: test `test_27_company_dependent` # Easy solution, need to flush write when changing a property. # Maybe it would be better to be able to compute all impacted cache value and update those instead - # Then clear_caches must be removed as well. + # Then clear_cache must be removed as well. self.env.flush_all() - self.clear_caches() + self.env.registry.clear_cache() return r @api.model_create_multi @@ -141,7 +141,7 @@ class Property(models.Model): if created_default: # DLE P44: test `test_27_company_dependent` self.env.flush_all() - self.clear_caches() + self.env.registry.clear_cache() return r def unlink(self): @@ -154,7 +154,7 @@ class Property(models.Model): default_deleted = self.env.cr.rowcount == 1 r = super().unlink() if default_deleted: - self.clear_caches() + self.env.registry.clear_cache() return r def get_by_record(self): diff --git a/odoo/addons/base/models/ir_qweb.py b/odoo/addons/base/models/ir_qweb.py index 83bf883eac1..08458497316 100644 --- a/odoo/addons/base/models/ir_qweb.py +++ b/odoo/addons/base/models/ir_qweb.py @@ -380,10 +380,12 @@ from dateutil.relativedelta import relativedelta from psycopg2.extensions import TransactionRollbackError from odoo import api, models, tools +from odoo.modules import registry from odoo.tools import config, safe_eval, pycompat from odoo.tools.constants import SUPPORTED_DEBUGGER, EXTERNAL_ASSET from odoo.tools.safe_eval import assert_valid_codeobj, _BUILTINS, to_opcodes, _EXPR_OPCODES, _BLACKLIST from odoo.tools.json import scriptsafe +from odoo.tools.lru import LRU from odoo.tools.misc import str2bool from odoo.tools.image import image_data_uri, FILETYPE_BASE64_MAGICWORD from odoo.http import request @@ -597,7 +599,7 @@ class IrQWeb(models.AbstractModel): @tools.conditional( 'xml' not in tools.config['dev_mode'], - tools.ormcache('template', 'tuple(self.env.context.get(k) for k in self._get_template_cache_keys())'), + tools.ormcache('template', 'tuple(self.env.context.get(k) for k in self._get_template_cache_keys())', cache='templates'), ) def _get_view_id(self, template): try: @@ -2485,7 +2487,6 @@ class IrQWeb(models.AbstractModel): """ generate value from the function if the result is not cached. """ if not cache_key: return get_value() - value = loaded_values and loaded_values.get(cache_key) if not value: value = self._get_cached_values(cache_key, get_value) @@ -2497,7 +2498,7 @@ class IrQWeb(models.AbstractModel): # in '_compile' method contains the write_date of all inherited views. @tools.conditional( 'xml' not in tools.config['dev_mode'], - tools.ormcache('cache_key'), + tools.ormcache('cache_key', cache='templates.cached_values'), ) def _get_cached_values(self, cache_key, get_value): """ generate value from the function if the result is not cached. """ @@ -2508,7 +2509,7 @@ class IrQWeb(models.AbstractModel): # in non-xml-debug mode we want assets to be cached forever, and the admin can force a cache clear # by restarting the server after updating the source code (or using the "Clear server cache" in debug tools) 'xml' not in tools.config['dev_mode'], - tools.ormcache('bundle', 'css', 'js', 'tuple(sorted(assets_params.items()))', 'rtl'), + tools.ormcache('bundle', 'css', 'js', 'tuple(sorted(assets_params.items()))', 'rtl', cache='assets'), ) def _generate_asset_links_cache(self, bundle, css=True, js=True, assets_params=None, rtl=False): return self._generate_asset_links(bundle, css, js, False, assets_params, rtl) @@ -2649,7 +2650,11 @@ def render(template_name, values, load, **options): """ class MockPool: db_name = None - _Registry__cache = {} + _Registry__caches = {cache_name: LRU(cache_size) for cache_name, cache_size in registry._REGISTRY_CACHES.items()} + _Registry__caches_groups = {} + for cache_name, cache in _Registry__caches.items(): + _Registry__caches_groups.setdefault(cache_name.split('.')[0], []).append(cache) + class MockIrQWeb(IrQWeb): _register = False # not visible in real registry diff --git a/odoo/addons/base/models/ir_rule.py b/odoo/addons/base/models/ir_rule.py index 4d713e35712..47a33e097bc 100644 --- a/odoo/addons/base/models/ir_rule.py +++ b/odoo/addons/base/models/ir_rule.py @@ -159,14 +159,9 @@ class IrRule(models.Model): v = tuple(v) yield v - @api.model - def clear_cache(self): - warnings.warn("Deprecated IrRule.clear_cache(), use IrRule.clear_caches() instead", DeprecationWarning) - self.clear_caches() - def unlink(self): res = super(IrRule, self).unlink() - self.clear_caches() + self.env.registry.clear_cache() return res @api.model_create_multi @@ -174,7 +169,7 @@ class IrRule(models.Model): res = super(IrRule, self).create(vals_list) # DLE P33: tests self.env.flush_all() - self.clear_caches() + self.env.registry.clear_cache() return res def write(self, vals): @@ -184,7 +179,7 @@ class IrRule(models.Model): # - odoo/addons/test_access_rights/tests/test_ir_rules.py # - odoo/addons/base/tests/test_orm.py (/home/dle/src/odoo/master-nochange-fp/odoo/addons/base/tests/test_orm.py) self.env.flush_all() - self.clear_caches() + self.env.registry.clear_cache() return res def _make_access_error(self, operation, records): diff --git a/odoo/addons/base/models/ir_ui_menu.py b/odoo/addons/base/models/ir_ui_menu.py index 0f4ab0e4cba..e35f6ff4d31 100644 --- a/odoo/addons/base/models/ir_ui_menu.py +++ b/odoo/addons/base/models/ir_ui_menu.py @@ -162,14 +162,14 @@ class IrUiMenu(models.Model): @api.model_create_multi def create(self, vals_list): - self.clear_caches() + self.env.registry.clear_cache() for values in vals_list: if 'web_icon' in values: values['web_icon_data'] = self._compute_web_icon_data(values.get('web_icon')) return super(IrUiMenu, self).create(vals_list) def write(self, values): - self.clear_caches() + self.env.registry.clear_cache() if 'web_icon' in values: values['web_icon_data'] = self._compute_web_icon_data(values.get('web_icon')) return super(IrUiMenu, self).write(values) @@ -194,7 +194,7 @@ class IrUiMenu(models.Model): direct_children = self.with_context(**extra).search([('parent_id', 'in', self.ids)]) direct_children.write({'parent_id': False}) - self.clear_caches() + self.env.registry.clear_cache() return super(IrUiMenu, self).unlink() def copy(self, default=None): diff --git a/odoo/addons/base/models/ir_ui_view.py b/odoo/addons/base/models/ir_ui_view.py index 3dc8833b932..7a5d46ac792 100644 --- a/odoo/addons/base/models/ir_ui_view.py +++ b/odoo/addons/base/models/ir_ui_view.py @@ -564,7 +564,7 @@ actual arch. values['arch_updated'] = False values.update(self._compute_defaults(values)) - self.clear_caches() + self.env.registry.clear_cache('templates') result = super(View, self.with_context(ir_ui_view_partial_validation=True)).create(vals_list) return result.with_env(self.env) @@ -580,7 +580,7 @@ actual arch. if custom_view: custom_view.unlink() - self.clear_caches() + self.env.registry.clear_cache('templates') if 'arch_db' in vals and not self.env.context.get('no_save_prev'): vals['arch_prev'] = self.arch_db @@ -2027,7 +2027,6 @@ actual arch. """ Return the list of context keys to use for caching ``_read_template``. """ return ['lang', 'inherit_branding', 'edit_translations'] - # apply ormcache_context decorator unless in dev mode... @api.model def _read_template(self, view_id): arch_tree = self.browse(view_id)._get_combined_arch() @@ -2058,11 +2057,6 @@ actual arch. """ return self.browse(self._get_view_id(view_ref)) - def clear_cache(self): - """ Deprecated, use `clear_caches` instead. """ - if 'xml' not in config['dev_mode']: - self.clear_caches() - def _contains_branded(self, node): return node.tag == 't'\ or 't-raw' in node.attrib\ @@ -2647,7 +2641,7 @@ class Model(models.AbstractModel): @api.model @tools.conditional( 'xml' not in config['dev_mode'], - tools.ormcache('self._get_view_cache_key(view_id, view_type, **options)'), + tools.ormcache('self._get_view_cache_key(view_id, view_type, **options)', cache='templates'), ) def _get_view_cache(self, view_id=None, view_type='form', **options): """ Get the view information ready to be cached diff --git a/odoo/addons/base/models/res_company.py b/odoo/addons/base/models/res_company.py index b91e16035f1..5a279f9b5a3 100644 --- a/odoo/addons/base/models/res_company.py +++ b/odoo/addons/base/models/res_company.py @@ -181,10 +181,6 @@ class Company(models.Model): for record in self: record.is_company_details_empty = not html2plaintext(record.company_details or '') - def cache_restart(self): - warnings.warn("Since 17.0, deprecated method, use `clear_caches` instead", DeprecationWarning, 2) - self.clear_caches() - @api.model_create_multi def create(self, vals_list): @@ -213,7 +209,7 @@ class Company(models.Model): for vals, partner in zip(no_partner_vals_list, partners): vals['partner_id'] = partner.id - self.clear_caches() + self.env.registry.clear_cache() companies = super().create(vals_list) # The write is made on the user to set it automatically in the multi company group. @@ -228,7 +224,7 @@ class Company(models.Model): return companies def write(self, values): - self.clear_caches() + self.env.registry.clear_cache() # Make sure that the selected currency is enabled if values.get('currency_id'): currency = self.env['res.currency'].browse(values['currency_id']) diff --git a/odoo/addons/base/models/res_country.py b/odoo/addons/base/models/res_country.py index 9ea59cf27de..9e5251f2654 100644 --- a/odoo/addons/base/models/res_country.py +++ b/odoo/addons/base/models/res_country.py @@ -115,11 +115,11 @@ class Country(models.Model): res = super().write(vals) if ('code' in vals or 'phone_code' in vals): # Intentionally simplified by not clearing the cache in create and unlink. - self.clear_caches() + self.env.registry.clear_cache() if 'address_view_id' in vals: # Changing the address view of the company must invalidate the view cached for res.partner # because of _view_get_address - self.env['res.partner'].clear_caches() + self.env.registry.clear_cache('templates') return res def get_address_fields(self): diff --git a/odoo/addons/base/models/res_lang.py b/odoo/addons/base/models/res_lang.py index 48a974735e2..ff8ae31778b 100644 --- a/odoo/addons/base/models/res_lang.py +++ b/odoo/addons/base/models/res_lang.py @@ -281,7 +281,7 @@ class Lang(models.Model): @api.model_create_multi def create(self, vals_list): - self.clear_caches() + self.env.registry.clear_cache() for vals in vals_list: if not vals.get('url_code'): vals['url_code'] = vals.get('iso_code') or vals['code'] @@ -301,7 +301,7 @@ class Lang(models.Model): res = super(Lang, self).write(vals) self.env.flush_all() - self.clear_caches() + self.env.registry.clear_cache() return res @api.ondelete(at_uninstall=True) @@ -316,7 +316,7 @@ class Lang(models.Model): raise UserError(_("You cannot delete the language which is Active!\nPlease de-activate the language first.")) def unlink(self): - self.clear_caches() + self.env.registry.clear_cache() return super(Lang, self).unlink() def format(self, percent, value, grouping=False, monetary=False): diff --git a/odoo/addons/base/models/res_users.py b/odoo/addons/base/models/res_users.py index 4bd88c1dce8..d929f224247 100644 --- a/odoo/addons/base/models/res_users.py +++ b/odoo/addons/base/models/res_users.py @@ -653,10 +653,10 @@ class Users(models.Model): # per-method / per-model caches have been removed so the various # clear_cache/clear_caches methods pretty much just end up calling - # Registry._clear_cache + # Registry.clear_cache invalidation_fields = self._get_invalidation_fields() if (invalidation_fields & values.keys()) or any(key.startswith('context_') for key in values): - self.clear_caches() + self.env.registry.clear_cache() return res @@ -666,7 +666,7 @@ class Users(models.Model): default_user_template = self.env.ref('base.default_user', False) if SUPERUSER_ID in self.ids: raise UserError(_('You can not remove the admin user as it is used internally for resources created by Odoo (updates, module installation, ...)')) - self.clear_caches() + self.env.registry.clear_cache() if (portal_user_template and portal_user_template in self) or (default_user_template and default_user_template in self): raise UserError(_('Deleting the template users is not allowed. Deleting this profile will compromise critical functionalities.')) @@ -848,7 +848,7 @@ class Users(models.Model): FROM res_users WHERE id=%%s""" % (session_fields), (self.id,)) if self.env.cr.rowcount != 1: - self.clear_caches() + self.env.registry.clear_cache() return False data_fields = self.env.cr.fetchone() # generate hmac key @@ -1357,7 +1357,7 @@ class GroupsView(models.Model): groups = super().create(vals_list) self._update_user_groups_view() # actions.get_bindings() depends on action records - self.env['ir.actions.actions'].clear_caches() + self.env.registry.clear_cache() return groups def write(self, values): @@ -1370,14 +1370,14 @@ class GroupsView(models.Model): if view_values0 != view_values1: self._update_user_groups_view() # actions.get_bindings() depends on action records - self.env['ir.actions.actions'].clear_caches() + self.env.registry.clear_cache() return res def unlink(self): res = super(GroupsView, self).unlink() self._update_user_groups_view() # actions.get_bindings() depends on action records - self.env['ir.actions.actions'].clear_caches() + self.env.registry.clear_cache() return res def _get_hidden_extra_categories(self): diff --git a/odoo/addons/base/tests/test_ir_actions.py b/odoo/addons/base/tests/test_ir_actions.py index dd65e50e2bb..689a864cc97 100644 --- a/odoo/addons/base/tests/test_ir_actions.py +++ b/odoo/addons/base/tests/test_ir_actions.py @@ -353,7 +353,7 @@ class TestCustomFields(common.TransactionCase): assert set(self.registry[self.MODEL]._fields) == fnames self.addCleanup(self.registry.reset_changes) - self.addCleanup(self.registry.clear_caches) + self.addCleanup(self.registry.clear_all_caches) super().setUp() @@ -550,7 +550,7 @@ class TestCustomFields(common.TransactionCase): model_id = self.env['ir.model']._get_id('res.partner') query_count = 40 with self.assertQueryCount(query_count): - self.env.registry.clear_caches() + self.env.registry.clear_cache() self.env['ir.model.fields'].create({ 'model_id': model_id, 'name': 'x_oh_box', @@ -561,7 +561,7 @@ class TestCustomFields(common.TransactionCase): # same with a related field, it only takes 8 extra queries with self.assertQueryCount(query_count + 8): - self.env.registry.clear_caches() + self.env.registry.clear_cache() self.env['ir.model.fields'].create({ 'model_id': model_id, 'name': 'x_oh_boy', diff --git a/odoo/addons/base/tests/test_ir_http.py b/odoo/addons/base/tests/test_ir_http.py index 692302c2632..7f894541c20 100644 --- a/odoo/addons/base/tests/test_ir_http.py +++ b/odoo/addons/base/tests/test_ir_http.py @@ -12,12 +12,12 @@ _logger = logging.getLogger(__name__) class TestIrHttpPerformances(TransactionCase): def test_routing_map_performance(self): - self.env['ir.http']._clear_routing_map() + self.env.registry.clear_cache('routing') # if the routing map was already generated it is possible that some compiled regex are in cache. # we want to mesure the cold state, when the worker just spawned, we need to empty the re cache re._cache.clear() - self.env['ir.http']._clear_routing_map() + self.env.registry.clear_cache('routing') start = time.time() self.env['ir.http'].routing_map() duration = time.time() - start diff --git a/odoo/addons/base/tests/test_ormcache.py b/odoo/addons/base/tests/test_ormcache.py index c2a80c06064..e92281ce62f 100644 --- a/odoo/addons/base/tests/test_ormcache.py +++ b/odoo/addons/base/tests/test_ormcache.py @@ -3,7 +3,7 @@ from odoo.tests.common import TransactionCase from odoo.tools import get_cache_key_counter - +from threading import Thread, Barrier class TestOrmcache(TransactionCase): def test_ormcache(self): @@ -17,7 +17,7 @@ class TestOrmcache(TransactionCase): miss = counter.miss # clear the caches of ir.model.data, retrieve its key and - IMD.clear_caches() + self.env.registry.clear_cache() self.assertNotIn(key, cache) # lookup some reference @@ -37,3 +37,63 @@ class TestOrmcache(TransactionCase): self.assertEqual(counter.hit, hit + 2) self.assertEqual(counter.miss, miss + 1) self.assertIn(key, cache) + + def test_invalidation(self): + self.assertEqual(self.env.registry.cache_invalidated, set()) + self.env.registry.clear_cache() + self.env.registry.clear_cache('templates') + self.assertEqual(self.env.registry.cache_invalidated, {'default', 'templates'}) + self.env.registry.reset_changes() + self.assertEqual(self.env.registry.cache_invalidated, set()) + self.env.registry.clear_cache('assets') + self.assertEqual(self.env.registry.cache_invalidated, {'assets'}) + self.env.registry.reset_changes() + self.assertEqual(self.env.registry.cache_invalidated, set()) + + def test_invalidation_thread_local(self): + # this test ensures that the registry.cache_invalidated set is thread local + + caches = ['default', 'templates', 'assets'] + nb_treads = len(caches) + + # use barriers to ensure threads synchronization + sync_clear_cache = Barrier(nb_treads, timeout=5) + sync_assert_equal = Barrier(nb_treads, timeout=5) + sync_reset = Barrier(nb_treads, timeout=5) + + operations = [] + def run(cache): + self.assertEqual(self.env.registry.cache_invalidated, set()) + + self.env.registry.clear_cache(cache) + operations.append('clear_cache') + sync_clear_cache.wait() + + self.assertEqual(self.env.registry.cache_invalidated, {cache}) + operations.append('assert_contains') + sync_assert_equal.wait() + + self.env.registry.reset_changes() + operations.append('reset_changes') + sync_reset.wait() + + self.assertEqual(self.env.registry.cache_invalidated, set()) + operations.append('assert_empty') + + # run all threads + threads = [] + for cache in caches: + threads.append(Thread(target=run, args=(cache,))) + for thread in threads: + thread.start() + for thread in threads: + thread.join() + + # ensure that the threads operations where executed in the expected order + self.assertEqual( + operations, + ['clear_cache'] * nb_treads + + ['assert_contains'] * nb_treads + + ['reset_changes'] * nb_treads + + ['assert_empty'] * nb_treads + ) diff --git a/odoo/addons/base/tests/test_qweb.py b/odoo/addons/base/tests/test_qweb.py index d31582ba0c0..a71795f17b3 100644 --- a/odoo/addons/base/tests/test_qweb.py +++ b/odoo/addons/base/tests/test_qweb.py @@ -1618,7 +1618,7 @@ class TestQWebBasic(TransactionCase): QWeb.with_context(preserve_comments=False)._render(view.id), markupsafe.Markup('

'), "Should not have the comment") - QWeb.clear_caches() + self.env.registry.clear_cache('templates') self.assertEqual( QWeb.with_context(preserve_comments=True)._render(view.id), markupsafe.Markup(f'

{comment}

'), @@ -1639,7 +1639,7 @@ class TestQWebBasic(TransactionCase): QWeb.with_context(preserve_comments=False)._render(view.id), markupsafe.Markup('

'), "Should not have the processing instruction") - QWeb.clear_caches() + self.env.registry.clear_cache('templates') self.assertEqual( QWeb.with_context(preserve_comments=True)._render(view.id), markupsafe.Markup(f'

{p_instruction}

'), diff --git a/odoo/addons/base/tests/test_res_users.py b/odoo/addons/base/tests/test_res_users.py index e7b046db9b1..0ce0dcbac19 100644 --- a/odoo/addons/base/tests/test_res_users.py +++ b/odoo/addons/base/tests/test_res_users.py @@ -209,15 +209,15 @@ class TestUsers(TransactionCase): request_patch.start() self.assertEqual(user.context_get()['lang'], 'fr_FR') - self.env.registry.clear_caches() + self.env.registry.clear_cache() user.lang = False self.assertEqual(user.context_get()['lang'], 'es_ES') - self.env.registry.clear_caches() + self.env.registry.clear_cache() request_patch.stop() self.assertEqual(user.context_get()['lang'], 'de_DE') - self.env.registry.clear_caches() + self.env.registry.clear_cache() company.lang = False self.assertEqual(user.context_get()['lang'], 'en_US') diff --git a/odoo/addons/base/tests/test_user_has_group.py b/odoo/addons/base/tests/test_user_has_group.py index 141ca697fa1..2592f24f538 100644 --- a/odoo/addons/base/tests/test_user_has_group.py +++ b/odoo/addons/base/tests/test_user_has_group.py @@ -252,17 +252,17 @@ class TestHasGroup(TransactionCase): user_b.write({"groups_id": [Command.link(group_C.id)]}) def test_has_group_cleared_cache_on_write(self): - self.registry._clear_cache() - self.assertFalse(self.registry._Registry__cache, "Ensure ormcache is empty") + self.env.registry.clear_cache() + self.assertFalse(self.registry._Registry__caches['default'], "Ensure ormcache is empty") def populate_cache(): self.test_user.has_group('test_user_has_group.group0') - self.assertTrue(self.registry._Registry__cache, "user.has_group cache must be populated") + self.assertTrue(self.registry._Registry__caches['default'], "user.has_group cache must be populated") populate_cache() self.env.ref(self.group0).write({"share": True}) - self.assertFalse(self.registry._Registry__cache, "Writing on group must invalidate user.has_group cache") + self.assertFalse(self.registry._Registry__caches['default'], "Writing on group must invalidate user.has_group cache") populate_cache() # call_cache_clearing_methods is called in res.groups.write to invalidate @@ -272,6 +272,6 @@ class TestHasGroup(TransactionCase): # the ormcache of method `user.has_group()` self.env['ir.model.access'].call_cache_clearing_methods() self.assertFalse( - self.registry._Registry__cache, + self.registry._Registry__caches['default'], "call_cache_clearing_methods() must invalidate user.has_group cache" ) diff --git a/odoo/addons/base/tests/test_views.py b/odoo/addons/base/tests/test_views.py index b46b7a93b5d..dae4cf28f83 100644 --- a/odoo/addons/base/tests/test_views.py +++ b/odoo/addons/base/tests/test_views.py @@ -334,12 +334,12 @@ class TestViewInheritance(ViewCase): self.assertEqual(counter.hit, hit) self.assertEqual(counter.miss, miss + 2) - with self.assertQueryCount(9): + with self.assertQueryCount(7): self.assertValid(""" """, inherit_id=base_view.id) - self.assertEqual(counter.hit, hit) - self.assertEqual(counter.miss, miss + 4) + self.assertEqual(counter.hit, hit + 2) + self.assertEqual(counter.miss, miss + 2) def test_view_validate_attrs_groups_query_count(self): _, _, counter = get_cache_key_counter(self.env['ir.model.data']._xmlid_lookup, 'base.group_system') @@ -356,14 +356,14 @@ class TestViewInheritance(ViewCase): self.assertEqual(counter.hit, hit) self.assertEqual(counter.miss, miss + 1) - with self.assertQueryCount(6): + with self.assertQueryCount(5): self.assertValid(""" """, inherit_id=base_view.id) - self.assertEqual(counter.hit, hit) - self.assertEqual(counter.miss, miss + 2) + self.assertEqual(counter.hit, hit + 1) + self.assertEqual(counter.miss, miss + 1) class TestApplyInheritanceSpecs(ViewCase): diff --git a/odoo/addons/test_access_rights/tests/test_ir_rules.py b/odoo/addons/test_access_rights/tests/test_ir_rules.py index dc2826123be..a10d12e7292 100644 --- a/odoo/addons/test_access_rights/tests/test_ir_rules.py +++ b/odoo/addons/test_access_rights/tests/test_ir_rules.py @@ -115,12 +115,12 @@ class TestRules(TransactionCase): self.assertFalse(ObjCateg.with_context(only_media=True).search([])) # record1 is food and is accessible with an empy context - ObjCateg.clear_caches() + self.env.registry.clear_cache() records = SomeObj.search([('id', '=', self.id1)]) self.assertTrue(records) # it should also be accessible as the context is not used when # searching for SomeObjs - ObjCateg.clear_caches() + self.env.registry.clear_cache() records = SomeObj.with_context(only_media=True).search([('id', '=', self.id1)]) self.assertTrue(records) diff --git a/odoo/addons/test_assetsbundle/tests/test_assetsbundle.py b/odoo/addons/test_assetsbundle/tests/test_assetsbundle.py index d5d7a001a19..96213e3ab13 100644 --- a/odoo/addons/test_assetsbundle/tests/test_assetsbundle.py +++ b/odoo/addons/test_assetsbundle/tests/test_assetsbundle.py @@ -125,7 +125,7 @@ class FileTouchable(AddonManifestPatched): @tagged('-at_install', 'post_install') class Test404(HttpCase): def get_assets_node_cache(self): - return [e for e in self.env.registry._Registry__cache.d if e[0] == 'ir.qweb' and '_generate_' in str(e[1])] #_generate_asset_nodes_cache + return [e for e in self.env.registry._Registry__caches['assets'].d if e[0] == 'ir.qweb' and '_generate_' in str(e[1])] #_generate_asset_nodes_cache def test_rollback(self): """ @@ -133,14 +133,14 @@ class Test404(HttpCase): this actually tests the add_post_rollback behaviour """ self.env['ir.attachment'].search([('url', '=like', '/web/assets/%web.assets_frontend%')]).unlink() - self.env.registry._clear_cache() + self.env.registry.clear_cache() self.url_open('/some_404_routes') self.assertFalse(self.get_assets_node_cache(), 'orm_cache should be emptied by rollback') self.assertFalse(self.env['ir.attachment'].search([('url', '=like', '/web/assets/%web.assets_frontend%%')]), "attachments should have been rollbacked") self.url_open('/') self.assertTrue(self.env['ir.attachment'].search([('url', '=like', '/web/assets/%web.assets_frontend%%')]), "attachments should have been generated as expected") self.assertTrue(self.get_assets_node_cache(), 'orm_cache should remain populated') - self.env.registry._clear_cache() + self.env.registry.clear_cache() class TestJavascriptAssetsBundle(FileTouchable): @classmethod @@ -1895,19 +1895,21 @@ class TestAssetsManifest(AddonManifestPatched): class AssetsNodeOrmCacheUsage(TransactionCase): def cache_keys(self): - keys = self.env.registry._Registry__cache.d + keys = self.env.registry._Registry__caches['assets'].d + asset_keys = [key for key in keys if key[0] == 'ir.asset' and '_get_asset_paths' in str(key[1])] # ignore topological sort entry qweb_keys = [key for key in keys if key[0] == 'ir.qweb'] return asset_keys, qweb_keys def test_assets_node_orm_cache_usage_debug(self): - self.env.registry._clear_cache() + self.env.registry.clear_cache('assets') asset_keys, qweb_keys = self.cache_keys() self.assertEqual(len(asset_keys), 0) self.assertEqual(len(qweb_keys), 0) self.env['ir.qweb']._get_asset_nodes('web.assets_backend') + asset_keys, qweb_keys = self.cache_keys() self.assertEqual(len(asset_keys), 1) self.assertEqual(len(qweb_keys), 1) @@ -1929,7 +1931,7 @@ class AssetsNodeOrmCacheUsage(TransactionCase): self.assertEqual(len(qweb_keys), 1) def test_assets_node_orm_cache_usage_file_type(self): - self.env.registry._clear_cache() + self.env.registry.clear_cache('assets') asset_keys, qweb_keys = self.cache_keys() self.assertEqual(len(asset_keys), 0) @@ -1954,7 +1956,7 @@ class AssetsNodeOrmCacheUsage(TransactionCase): def test_assets_node_orm_cache_usage_lang(self): - self.env.registry._clear_cache() + self.env.registry.clear_cache('assets') self.env['res.lang']._activate_lang('ar_SY') self.env['res.lang']._activate_lang('fr_FR') self.env['res.lang']._activate_lang('en_US') @@ -1981,7 +1983,7 @@ class AssetsNodeOrmCacheUsage(TransactionCase): def test_assets_node_orm_cache_usage_website(self): if self.env['ir.module.module'].search([('name', '=', 'website'), ('state', '=', 'uninstalled')]): return # only makes sence if website is installed - self.env.registry._clear_cache() + self.env.registry.clear_cache('assets') asset_keys, qweb_keys = self.cache_keys() self.assertEqual(len(asset_keys), 0) @@ -1998,7 +2000,7 @@ class AssetsNodeOrmCacheUsage(TransactionCase): self.assertEqual(len(qweb_keys), 2) def test_assets_node_orm_cache_usage_node_flags(self): - self.env.registry._clear_cache() + self.env.registry.clear_cache('assets') asset_keys, qweb_keys = self.cache_keys() self.assertEqual(len(asset_keys), 0) diff --git a/odoo/addons/test_impex/tests/test_load.py b/odoo/addons/test_impex/tests/test_load.py index 69e57929594..64c3b69e6f1 100644 --- a/odoo/addons/test_impex/tests/test_load.py +++ b/odoo/addons/test_impex/tests/test_load.py @@ -40,7 +40,7 @@ class ImporterCase(common.TransactionCase): def setUp(self): super(ImporterCase, self).setUp() self.model = self.env[self.model_name] - self.env['ir.model.data'].clear_caches() + self.env.registry.clear_cache() self.cr.cache.clear() def import_(self, fields, rows, context=None): @@ -1114,7 +1114,7 @@ class test_realworld(SavepointCaseWithUserDemo): """ The content of the o2m field's dict needs to go through conversion as it may be composed of convertables or other relational fields """ - self.env['ir.model.data'].clear_caches() + self.env.registry.clear_cache() Model = self.env['export.one2many.recursive'] result = Model.load( ['value', 'child/const', 'child/child1/str', 'child/child2/value'], @@ -1141,7 +1141,7 @@ class test_realworld(SavepointCaseWithUserDemo): [12]) def test_o2m_subfields_fail_by_implicit_id(self): - self.env['ir.model.data'].clear_caches() + self.env.registry.clear_cache() Model = self.env['export.one2many.recursive'] result = Model.with_context(import_file=True).load( ['child/child1/parent_id'], diff --git a/odoo/addons/test_new_api/tests/test_new_fields.py b/odoo/addons/test_new_api/tests/test_new_fields.py index 4f14974ec44..2e3f3fc227b 100644 --- a/odoo/addons/test_new_api/tests/test_new_fields.py +++ b/odoo/addons/test_new_api/tests/test_new_fields.py @@ -25,7 +25,7 @@ class TestFields(TransactionCaseWithUserDemo): def setUp(self): # for tests methods that create custom models/fields self.addCleanup(self.registry.reset_changes) - self.addCleanup(self.registry.clear_caches) + self.addCleanup(self.registry.clear_all_caches) super(TestFields, self).setUp() self.env.ref('test_new_api.discussion_0').write({'participants': [Command.link(self.user_demo.id)]}) # YTI FIX ME: The cache shouldn't be inconsistent (rco is gonna fix it) diff --git a/odoo/http.py b/odoo/http.py index c1240270621..0bc5c7284d7 100644 --- a/odoo/http.py +++ b/odoo/http.py @@ -2054,7 +2054,7 @@ class Application: def get_db_router(self, db): if not db: return self.nodb_routing_map - return request.registry['ir.http'].routing_map() + return request.env['ir.http'].routing_map() @lazy_property def geoip_city_db(self): diff --git a/odoo/models.py b/odoo/models.py index 9ba686f94d1..49996a1cb83 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -1788,9 +1788,10 @@ class BaseModel(metaclass=MetaModel): """ Clear the caches This clears the caches associated to methods decorated with - ``tools.ormcache`` or ``tools.ormcache_multi``. + ``tools.ormcache``. """ - cls.pool._clear_cache() + warnings.warn("Deprecated model.clear_cache(), use registry.clear_cache() instead", DeprecationWarning) + cls.pool.clear_all_cache() @api.model def _read_group(self, domain, groupby=(), aggregates=(), having=(), offset=0, limit=None, order=None): diff --git a/odoo/modules/loading.py b/odoo/modules/loading.py index b2463e75b5a..53dbc7dd48d 100644 --- a/odoo/modules/loading.py +++ b/odoo/modules/loading.py @@ -279,8 +279,6 @@ def load_module_graph(env, graph, status=None, perform_checks=True, if not needs_update: registry.setup_models(env.cr) # Python tests - env['ir.http']._clear_routing_map() # force routing map to be rebuilt - tests_t0, tests_q0 = time.time(), odoo.sql_db.sql_counter test_results = loader.run_suite(suite, module_name) report.update(test_results) diff --git a/odoo/modules/registry.py b/odoo/modules/registry.py index 77f8b1b23b6..55cf2f7f58a 100644 --- a/odoo/modules/registry.py +++ b/odoo/modules/registry.py @@ -9,6 +9,8 @@ from collections.abc import Mapping from contextlib import closing, contextmanager from functools import partial from operator import attrgetter + +import inspect import logging import os import threading @@ -22,8 +24,11 @@ from odoo.modules.db import FunctionStatus from odoo.osv.expression import get_unaccent_wrapper from .. import SUPERUSER_ID from odoo.sql_db import TestCursor -from odoo.tools import (config, existing_tables, lazy_classproperty, - lazy_property, sql, Collector, OrderedSet) +from odoo.tools import ( + config, existing_tables, lazy_classproperty, + lazy_property, sql, Collector, OrderedSet, + format_frame +) from odoo.tools.func import locked from odoo.tools.lru import LRU @@ -31,6 +36,17 @@ _logger = logging.getLogger(__name__) _schema = logging.getLogger('odoo.schema') +_REGISTRY_CACHES = { + 'default': 8192, + 'assets': 512, # arbitrary + 'templates': 1024, # arbitrary + 'templates.cached_values': 2048, # arbitrary + 'routing': 1024, # 2 entries per website + 'routing.rewrites': 8192, # url_rewrite entries + 'xmlid': 1024, +} + + class Registry(Mapping): """ Model registry for a particular database. @@ -118,7 +134,10 @@ class Registry(Mapping): self._fields_by_model = None self._ordinary_tables = None self._constraint_queue = deque() - self.__cache = LRU(8192) + self.__caches = {cache_name: LRU(cache_size) for cache_name, cache_size in _REGISTRY_CACHES.items()} + self.__caches_by_prefix = {} + for cache_name, cache in self.__caches.items(): + self.__caches_by_prefix.setdefault(cache_name.split('.')[0], []).append(cache) # modules fully loaded (maintained during init phase by `loading` module) self._init_modules = set() @@ -151,7 +170,7 @@ class Registry(Mapping): # The `base_cache_signaling sequence` indicates all caches must be # invalidated (i.e. cleared). self.registry_sequence = None - self.cache_sequence = None + self.cache_sequences = {} # Flags indicating invalidation of the registry or the cache. self._invalidation_flags = threading.local() @@ -233,7 +252,8 @@ class Registry(Mapping): from .. import models # clear cache to ensure consistency, but do not signal it - self.__cache.clear() + for cache in self.__caches.values(): + cache.clear() lazy_property.reset_all(self) self._field_trigger_trees.clear() @@ -263,7 +283,8 @@ class Registry(Mapping): model._unregister_hook() # clear cache to ensure consistency, but do not signal it - self.__cache.clear() + for cache in self.__caches.values(): + cache.clear() lazy_property.reset_all(self) self._field_trigger_trees.clear() @@ -677,17 +698,33 @@ class Registry(Mapping): for table in missing_tables: _logger.error("Model %s has no table.", table2model[table]) - def _clear_cache(self): - """ Clear the cache and mark it as invalidated. """ - self.__cache.clear() - self.cache_invalidated = True - - def clear_caches(self): + def clear_cache(self, *cache_names): """ Clear the caches associated to methods decorated with - ``tools.ormcache`` or ``tools.ormcache_multi`` for all the models. + ``tools.ormcache``if cache is in `cache_name` subset. """ + cache_names = cache_names or ('default',) + assert not any('.' in cache_name for cache_name in cache_names) + for cache_name in cache_names: + for cache in self.__caches_by_prefix[cache_name]: + cache.clear() + self.cache_invalidated.add(cache_name) + + # log information about invalidation_cause + caller_info = format_frame(inspect.currentframe().f_back) + log = _logger.info if self.loaded else _logger.debug + log('Invalidating %s model caches from %s', ','.join(cache_names), caller_info) + + def clear_all_caches(self): + """ Clear the caches associated to methods decorated with + ``tools.ormcache``. """ - for model in self.models.values(): - model.clear_caches() + for cache_name, caches in self.__caches_by_prefix.items(): + for cache in caches: + cache.clear() + self.cache_invalidated.add(cache_name) + + caller_info = format_frame(inspect.currentframe().f_back) + log = _logger.info if self.loaded else _logger.debug + log('Invalidating all model caches from %s', caller_info) def is_an_ordinary_table(self, model): """ Return whether the given model has an ordinary table. """ @@ -719,11 +756,11 @@ class Registry(Mapping): @property def cache_invalidated(self): """ Determine whether the current thread has modified the cache. """ - return getattr(self._invalidation_flags, 'cache', False) - - @cache_invalidated.setter - def cache_invalidated(self, value): - self._invalidation_flags.cache = value + try: + return self._invalidation_flags.cache + except AttributeError: + names = self._invalidation_flags.cache = set() + return names def setup_signaling(self): """ Setup the inter-process signaling on this registry. """ @@ -733,21 +770,35 @@ class Registry(Mapping): with self.cursor() as cr: # The `base_registry_signaling` sequence indicates when the registry # must be reloaded. - # The `base_cache_signaling` sequence indicates when all caches must + # The `base_cache_signaling_...` sequences indicates when caches must # be invalidated (i.e. cleared). - cr.execute("SELECT sequence_name FROM information_schema.sequences WHERE sequence_name='base_registry_signaling'") - if not cr.fetchall(): - cr.execute("CREATE SEQUENCE base_registry_signaling INCREMENT BY 1 START WITH 1") - cr.execute("SELECT nextval('base_registry_signaling')") - cr.execute("CREATE SEQUENCE base_cache_signaling INCREMENT BY 1 START WITH 1") - cr.execute("SELECT nextval('base_cache_signaling')") + sequence_names = ('base_registry_signaling', *(f'base_cache_signaling_{cache_name}' for cache_name in self.__caches_by_prefix)) + cr.execute("SELECT sequence_name FROM information_schema.sequences WHERE sequence_name IN %s", [sequence_names]) + existing_sequences = tuple(s[0] for s in cr.fetchall()) # could be a set but not efficient with such a little list - cr.execute(""" SELECT base_registry_signaling.last_value, - base_cache_signaling.last_value - FROM base_registry_signaling, base_cache_signaling""") - self.registry_sequence, self.cache_sequence = cr.fetchone() - _logger.debug("Multiprocess load registry signaling: [Registry: %s] [Cache: %s]", - self.registry_sequence, self.cache_sequence) + for sequence_name in sequence_names: + if sequence_name not in existing_sequences: + _sequence_name = sql.Identifier(sequence_name) # even if sequence_name should be safe, will avoid flagging the sql_injection linter + cr.execute(sql.SQL("CREATE SEQUENCE {} INCREMENT BY 1 START WITH 1").format(_sequence_name)) + cr.execute("SELECT nextval(%s)", [sequence_name]) + + db_registry_sequence, db_cache_sequences = self.get_sequences(cr) + self.registry_sequence = db_registry_sequence + self.cache_sequences.update(db_cache_sequences) + + _logger.debug("Multiprocess load registry signaling: [Registry: %s] %s", + self.registry_sequence, ' '.join('[Cache %s: %s]' % cs for cs in self.cache_sequences.items())) + + def get_sequences(self, cr): + cache_sequences_query = ', '.join([f'base_cache_signaling_{cache_name}' for cache_name in self.__caches_by_prefix]) + cache_sequences_values_query = ',\n'.join([f'base_cache_signaling_{cache_name}.last_value' for cache_name in self.__caches_by_prefix]) + cr.execute(f""" + SELECT base_registry_signaling.last_value, {cache_sequences_values_query} + FROM base_registry_signaling, {cache_sequences_query} + """) + registry_sequence, *cache_sequences_values = cr.fetchone() + cache_sequences = dict(zip(self.__caches_by_prefix, cache_sequences_values)) + return registry_sequence, cache_sequences def check_signaling(self): """ Check whether the registry has changed, and performs all necessary @@ -757,28 +808,28 @@ class Registry(Mapping): return self with closing(self.cursor()) as cr: - cr.execute(""" SELECT base_registry_signaling.last_value, - base_cache_signaling.last_value - FROM base_registry_signaling, base_cache_signaling""") - r, c = cr.fetchone() - _logger.debug("Multiprocess signaling check: [Registry - %s -> %s] [Cache - %s -> %s]", - self.registry_sequence, r, self.cache_sequence, c) + db_registry_sequence, db_cache_sequences = self.get_sequences(cr) + changes = '' # Check if the model registry must be reloaded - if self.registry_sequence != r: + if self.registry_sequence != db_registry_sequence: _logger.info("Reloading the model registry after database signaling.") self = Registry.new(self.db_name) + self.registry_sequence = db_registry_sequence + if _logger.isEnabledFor(logging.DEBUG): + changes += "[Registry - %s -> %s]" % (self.registry_sequence, db_registry_sequence) # Check if the model caches must be invalidated. - elif self.cache_sequence != c: - _logger.info("Invalidating all model caches after database signaling.") - self.clear_caches() - - # prevent re-signaling the clear_caches() above, or any residual one that - # would be inherited from the master process (first request in pre-fork mode) - self.cache_invalidated = False - - self.registry_sequence = r - self.cache_sequence = c - + else: + for cache_name, cache_sequence in self.cache_sequences.items(): + expected_sequence = db_cache_sequences[cache_name] + if cache_sequence != expected_sequence: + _logger.info("Invalidating %s model caches after database signaling.", cache_name) + for cache in self.__caches_by_prefix[cache_name]: # don't call clear_cache to avoid signal loop + cache.clear() + self.cache_sequences[cache_name] = expected_sequence + if _logger.isEnabledFor(logging.DEBUG): + changes += "[Cache %s - %s -> %s]" % (cache_name, cache_sequence, expected_sequence) + if changes: + _logger.debug("Multiprocess signaling check: %s", changes) return self def signal_changes(self): @@ -786,10 +837,10 @@ class Registry(Mapping): if self.in_test_mode(): if self.registry_invalidated: self.registry_sequence += 1 - if self.cache_invalidated: - self.cache_sequence += 1 + for cache_name in self.cache_invalidated or (): + self.cache_sequences[cache_name] += 1 self.registry_invalidated = False - self.cache_invalidated = False + self.cache_invalidated.clear() return if self.registry_invalidated: @@ -803,11 +854,12 @@ class Registry(Mapping): elif self.cache_invalidated: _logger.info("At least one model cache has been invalidated, signaling through the database.") with closing(self.cursor()) as cr: - cr.execute("select nextval('base_cache_signaling')") - self.cache_sequence = cr.fetchone()[0] + for cache_name in self.cache_invalidated: + cr.execute("select nextval(%s)", [f'base_cache_signaling_{cache_name}']) + self.cache_sequences[cache_name] = cr.fetchone()[0] self.registry_invalidated = False - self.cache_invalidated = False + self.cache_invalidated.clear() def reset_changes(self): """ Reset the registry and cancel all invalidations. """ @@ -816,8 +868,10 @@ class Registry(Mapping): self.setup_models(cr) self.registry_invalidated = False if self.cache_invalidated: - self.__cache.clear() - self.cache_invalidated = False + for cache_name in self.cache_invalidated: + for cache in self.__caches_by_prefix[cache_name]: + cache.clear() + self.cache_invalidated.clear() @contextmanager def manage_changes(self): diff --git a/odoo/tests/common.py b/odoo/tests/common.py index 6c6644a4f70..ab381bad1c3 100644 --- a/odoo/tests/common.py +++ b/odoo/tests/common.py @@ -749,10 +749,11 @@ class TransactionCase(BaseCase): if (cls.registry_start_sequence != cls.registry.registry_sequence) or cls.registry.registry_invalidated: with cls.registry.cursor() as cr: cls.registry.setup_models(cr) - cls.registry.registry_invalidated = False + cls.registry.registry_invalidated = False + cls.registry.clear_all_caches() + cls.registry.cache_invalidated.clear() cls.addClassCleanup(reset_changes) - cls.addClassCleanup(cls.registry.clear_caches) cls.cr = cls.registry.cursor() cls.addClassCleanup(cls.cr.close) @@ -771,7 +772,7 @@ class TransactionCase(BaseCase): self.addCleanup(envs.update, list(envs)) self.addCleanup(envs.clear) - self.addCleanup(self.registry.clear_caches) + self.addCleanup(self.registry.clear_all_caches) # This prevents precommit functions and data from piling up # until cr.flush is called in 'assertRaises' clauses @@ -810,7 +811,7 @@ class SingleTransactionCase(BaseCase): super().setUpClass() cls.registry = odoo.registry(get_db_name()) cls.addClassCleanup(cls.registry.reset_changes) - cls.addClassCleanup(cls.registry.clear_caches) + cls.addClassCleanup(cls.registry.clear_all_caches) cls.cr = cls.registry.cursor() cls.addClassCleanup(cls.cr.close) diff --git a/odoo/tools/cache.py b/odoo/tools/cache.py index 912affbba6d..b1939eff696 100644 --- a/odoo/tools/cache.py +++ b/odoo/tools/cache.py @@ -6,6 +6,7 @@ from collections import Counter, defaultdict from decorator import decorator from inspect import signature, Parameter import logging +import warnings unsafe_eval = eval @@ -52,12 +53,13 @@ class ormcache(object): def __init__(self, *args, **kwargs): self.args = args self.skiparg = kwargs.get('skiparg') + self.cache_name = kwargs.get('cache', 'default') def __call__(self, method): self.method = method self.determine_key() lookup = decorator(self.lookup, method) - lookup.clear_cache = self.clear + lookup.ormcache = self return lookup def determine_key(self): @@ -82,7 +84,7 @@ class ormcache(object): def lru(self, model): counter = STAT[(model.pool.db_name, model._name, self.method)] - return model.pool._Registry__cache, (model._name, self.method), counter + return model.pool._Registry__caches[self.cache_name], (model._name, self.method), counter def lookup(self, method, *args, **kwargs): d, key0, counter = self.lru(args[0]) @@ -102,7 +104,8 @@ class ormcache(object): def clear(self, model, *args): """ Clear the registry cache """ - model.pool._clear_cache() + warnings.warn('Deprecated method ormcache.clear(model, *args), use registry.clear_cache() instead') + model.pool.clear_all_cache() class ormcache_context(ormcache): @@ -133,81 +136,6 @@ class ormcache_context(ormcache): self.key = unsafe_eval(code) -class ormcache_multi(ormcache): - """ This LRU cache decorator is a variant of :class:`ormcache`, with an - extra parameter ``multi`` that gives the name of a parameter. Upon call, the - corresponding argument is iterated on, and every value leads to a cache - entry under its own key. - """ - def __init__(self, *args, **kwargs): - super(ormcache_multi, self).__init__(*args, **kwargs) - self.multi = kwargs['multi'] - - def determine_key(self): - """ Determine the function that computes a cache key from arguments. """ - assert self.skiparg is None, "ormcache_multi() no longer supports skiparg" - assert isinstance(self.multi, str), "ormcache_multi() parameter multi must be an argument name" - - super(ormcache_multi, self).determine_key() - - # key_multi computes the extra element added to the key - sign = signature(self.method) - args = ', '.join( - str(params.replace(annotation=Parameter.empty, default=Parameter.empty)) - for params in sign.parameters.values() - ) - code_multi = "lambda %s: %s" % (args, self.multi) - self.key_multi = unsafe_eval(code_multi) - - # self.multi_pos is the position of self.multi in args - self.multi_pos = list(sign.parameters).index(self.multi) - - def lookup(self, method, *args, **kwargs): - d, key0, counter = self.lru(args[0]) - base_key = key0 + self.key(*args, **kwargs) - ids = self.key_multi(*args, **kwargs) - result = {} - missed = [] - - # first take what is available in the cache - for i in ids: - key = base_key + (i,) - try: - result[i] = d[key] - counter.hit += 1 - except Exception: - counter.miss += 1 - missed.append(i) - - if missed: - # call the method for the ids that were not in the cache; note that - # thanks to decorator(), the multi argument will be bound and passed - # positionally in args. - args = list(args) - args[self.multi_pos] = missed - result.update(method(*args, **kwargs)) - - # store those new results back in the cache - for i in missed: - key = base_key + (i,) - d[key] = result[i] - - return result - - -class dummy_cache(object): - """ Cache decorator replacement to actually do no caching. """ - def __init__(self, *l, **kw): - pass - - def __call__(self, fn): - fn.clear_cache = self.clear - return fn - - def clear(self, *l, **kw): - pass - - def log_ormcache_stats(sig=None, frame=None): """ Log statistics of ormcache usage by database, model, and method. """ from odoo.modules.registry import Registry @@ -216,26 +144,30 @@ def log_ormcache_stats(sig=None, frame=None): me = threading.current_thread() me_dbname = getattr(me, 'dbname', 'n/a') - for dbname, reg in sorted(Registry.registries.d.items()): - # set logger prefix to dbname - me.dbname = dbname - entries = Counter(k[:2] for k in reg._Registry__cache.d) + def _log_ormcache_stats(cache_name, cache): + entries = Counter(k[:2] for k in cache.d) # show entries sorted by model name, method name for key in sorted(entries, key=lambda key: (key[0], key[1].__name__)): model, method = key stat = STAT[(dbname, model, method)] _logger.info( - "%6d entries, %6d hit, %6d miss, %6d err, %4.1f%% ratio, for %s.%s", - entries[key], stat.hit, stat.miss, stat.err, stat.ratio, model, method.__name__, + "%s, %6d entries, %6d hit, %6d miss, %6d err, %4.1f%% ratio, for %s.%s", + cache_name.rjust(25), entries[key], stat.hit, stat.miss, stat.err, stat.ratio, model, method.__name__, ) + for dbname, reg in sorted(Registry.registries.d.items()): + # set logger prefix to dbname + me.dbname = dbname + for cache_name, cache in reg._Registry__caches.items(): + _log_ormcache_stats(cache_name, cache) + me.dbname = me_dbname def get_cache_key_counter(bound_method, *args, **kwargs): """ Return the cache, key and stat counter for the given call. """ model = bound_method.__self__ - ormcache = bound_method.clear_cache.__self__ + ormcache = bound_method.ormcache cache, key0, counter = ormcache.lru(model) key = key0 + ormcache.key(model, *args, **kwargs) return cache, key, counter diff --git a/odoo/tools/misc.py b/odoo/tools/misc.py index 5c068c11fb0..ee2dfe3d9bd 100644 --- a/odoo/tools/misc.py +++ b/odoo/tools/misc.py @@ -1786,3 +1786,8 @@ def get_flag(country_code: str) -> str: This emoji is composed of the two regional indicator emoji of the country code. """ return "".join(chr(int(f"1f1{ord(c)+165:02x}", base=16)) for c in country_code) + + +def format_frame(frame): + code = frame.f_code + return f'{code.co_name} {code.co_filename}:{frame.f_lineno}'