[FIX] race condition in ir.ui.menu leading to incomplete menus in web client

See bug, issue occurs with variable frequency when changing between
simple and extended views in a user account (non-administrator at
least): saving the user account leads to a clearing of the menu cache,
this is followed by two search/read in parallel (one to get the full
menu listing and one to get the list of applications for the home
page), which leads to ir_ui_menu.search (thus
ir_ui_menu._filter_visible_menus) being called concurrently, and this
apparently somehow wrecks havoc on some browse_record's caches
yielding to incoherent behaviors (menus which do have children in db
not having children in the browse_record, and thus being pruned from
the list of menus).

Putting a big lock around 1. clear_cache (just in case) and
2. _filter_visible_menu (to make cache-filling essentially atomic)
seems to solve the issue or at least make it disappear, ideally more
time should be spent understanding what breaks in browse_record.

A reentrant lock is needed as _filter_visible_menu may recurse when
accessing e.g. a menu's child_id (which yield a
search([parent_id=menu.id]) and thus a _filter_visible_menu)

lp bug: https://launchpad.net/bugs/920332 fixed

bzr revid: xmo@openerp.com-20120125115823-rpu03zdv14t11lp3
This commit is contained in:
Xavier Morel 2012-01-25 12:58:23 +01:00
parent d181a5e7ac
commit 3049760fe6
1 changed files with 50 additions and 46 deletions

View File

@ -22,6 +22,7 @@
import base64
import re
import threading
import tools
import openerp.modules
@ -40,65 +41,68 @@ class ir_ui_menu(osv.osv):
_name = 'ir.ui.menu'
def __init__(self, *args, **kwargs):
self._cache = {}
self.cache_lock = threading.RLock()
self.clear_cache()
r = super(ir_ui_menu, self).__init__(*args, **kwargs)
self.pool.get('ir.model.access').register_cache_clearing_method(self._name, 'clear_cache')
return r
def clear_cache(self):
# radical but this doesn't frequently happen
self._cache = {}
with self.cache_lock:
# radical but this doesn't frequently happen
self._cache = {}
def _filter_visible_menus(self, cr, uid, ids, context=None):
"""Filters the give menu ids to only keep the menu items that should be
visible in the menu hierarchy of the current user.
Uses a cache for speeding up the computation.
"""
modelaccess = self.pool.get('ir.model.access')
user_groups = set(self.pool.get('res.users').read(cr, 1, uid, ['groups_id'])['groups_id'])
result = []
for menu in self.browse(cr, uid, ids, context=context):
# this key works because user access rights are all based on user's groups (cfr ir_model_access.check)
key = (cr.dbname, menu.id, tuple(user_groups))
if key in self._cache:
if self._cache[key]:
result.append(menu.id)
#elif not menu.groups_id and not menu.action:
# result.append(menu.id)
continue
self._cache[key] = False
if menu.groups_id:
restrict_to_groups = [g.id for g in menu.groups_id]
if not user_groups.intersection(restrict_to_groups):
continue
#result.append(menu.id)
#self._cache[key] = True
#continue
if menu.action:
# we check if the user has access to the action of the menu
data = menu.action
if data:
model_field = { 'ir.actions.act_window': 'res_model',
'ir.actions.report.xml': 'model',
'ir.actions.wizard': 'model',
'ir.actions.server': 'model_id',
}
field = model_field.get(menu.action._name)
if field and data[field]:
if not modelaccess.check(cr, uid, data[field], 'read', False):
continue
else:
# if there is no action, it's a 'folder' menu
if not menu.child_id:
# not displayed if there is no children
with self.cache_lock:
modelaccess = self.pool.get('ir.model.access')
user_groups = set(self.pool.get('res.users').read(cr, 1, uid, ['groups_id'])['groups_id'])
result = []
for menu in self.browse(cr, uid, ids, context=context):
# this key works because user access rights are all based on user's groups (cfr ir_model_access.check)
key = (cr.dbname, menu.id, tuple(user_groups))
if key in self._cache:
if self._cache[key]:
result.append(menu.id)
#elif not menu.groups_id and not menu.action:
# result.append(menu.id)
continue
result.append(menu.id)
self._cache[key] = True
return result
self._cache[key] = False
if menu.groups_id:
restrict_to_groups = [g.id for g in menu.groups_id]
if not user_groups.intersection(restrict_to_groups):
continue
#result.append(menu.id)
#self._cache[key] = True
#continue
if menu.action:
# we check if the user has access to the action of the menu
data = menu.action
if data:
model_field = { 'ir.actions.act_window': 'res_model',
'ir.actions.report.xml': 'model',
'ir.actions.wizard': 'model',
'ir.actions.server': 'model_id',
}
field = model_field.get(menu.action._name)
if field and data[field]:
if not modelaccess.check(cr, uid, data[field], 'read', False):
continue
else:
# if there is no action, it's a 'folder' menu
if not menu.child_id:
# not displayed if there is no children
continue
result.append(menu.id)
self._cache[key] = True
return result
def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
if context is None: