Merge lp:~therp-nl/openobject-server/7.0-lp1238042-browse_record_refresh_clear_ids into lp:openobject-server/7.0

Proposed by Stefan Rijnhart (Opener)
Status: Rejected
Rejected by: Olivier Dony (Odoo)
Proposed branch: lp:~therp-nl/openobject-server/7.0-lp1238042-browse_record_refresh_clear_ids
Merge into: lp:openobject-server/7.0
Diff against target: 36 lines (+12/-0)
1 file modified
openerp/osv/orm.py (+12/-0)
To merge this branch: bzr merge lp:~therp-nl/openobject-server/7.0-lp1238042-browse_record_refresh_clear_ids
Reviewer Review Type Date Requested Status
OpenERP Core Team Pending
Review via email: mp+190402@code.launchpad.net

Description of the change

This branch fixes problems with inaccessible record ids ending up in the browse cache when fetching many2one relationships. A read operation is attempted on these records when fetching new data such as function fields or binary fields or any field after a refresh(). Here for example, the parent company with id 3 is inaccessible but a read operation is still attempted on it after it was added when reading its value from the record's parent_id, together will all other plain fields:

(Pdb) co = self.pool.get('res.company').browse(cr, uid, 1)
(Pdb) co.currency_id.id
1
(Pdb) co._data.keys()
[1, 3]
(Pdb) co.street
*** except_orm: (u'Access Denied', u'The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: Companies, Operation: read)')

This branch solves the problem by filtering out inaccessible records from the cache after values for a many2one field were fetched.

Revno. 5101 is green on runbot.

To post a comment you must log in.
Revision history for this message
Stefan Rijnhart (Opener) (stefan-opener) wrote :

Setting to work in progress as Olivier Dony pointed out that this prevents fetching a list of browse records efficiently.

5102. By Stefan Rijnhart (Opener)

[RFR] s/o2m/m2o/

Revision history for this message
Stefan Rijnhart (Opener) (stefan-opener) wrote :

Retracting because an alternative fix by Olivier was merged.

Revision history for this message
Olivier Dony (Odoo) (odo-openerp) wrote :

Hi Stefan,

As you've seen, I tried to come up with a minimalist patch[1] to fix this behavior without incurring the penalty of a second level of ir.rule check for each browse prefetch.
Your LBYL approach works fine but most models have multiple m2o relationships, so I believe the cost would be significant - probably a couple of extra SQL roundtrips for each browse fetch (Discl: I did not benchmark your patch). Browse_records are designed to only cost 1 or 2 SQL queries for the basic `for r in self.browse(..)` loop that only involves database-stored fields, regardless of the number of fields and records being browsed. If you add a couple of second-level ir.rule check queries to that every time, it seems like it could make browse() perform a couple of times slower on average.

The patch I wrote uses an EAFP approach instead and tries to detect the cases where the prefetching system causes an "artificial" access error. When that happens, it retries the operation only for the record being browsed, effectively turning off prefetching. As the bug occurred rather seldom, this should hopefully not affect average performance in a significant way. However when it happens, prefetching is silently [2] disabled, and further browse() usage will be slower. This may or may not be desirable, depending on occurrence frequency.

Watching the log for the "Prefetching attempt for fields [..] on [model] failed..." should tell us if that happens too often.

Thanks a lot for your work on this issue, I hope the patch works for you!

[1] For the record: server 7.0 revno 5136 rev-id: <email address hidden>
[2] It's actually logged at INFO level, but the user/developer may not notice it - possibly missing a bad ir.rule definition that uselessly impacts performance.

Unmerged revisions

5102. By Stefan Rijnhart (Opener)

[RFR] s/o2m/m2o/

5101. By Stefan Rijnhart (Opener)

[FIX] Bypass model access check to allow for mail.message's bypass of
 this mechanism

5100. By Stefan Rijnhart (Opener)

[FIX] Don't _search when superuser, as this causes a loop when records
 are browsed as record rules are applied

5099. By Stefan Rijnhart (Opener)

[RFT] Filter out inaccessible ids from the browse record cache
 after fetching

5098. By Stefan Rijnhart (Opener)

[RVT] Undo previous change as it does not allow to efficiently fetch
 data for a list of browse records (thanks to Olivier Dony for
  pointing that out)

5097. By Stefan Rijnhart (Opener)

[FIX] Also fix an additional access error when the id of an inaccessible
 record is in the _data dictionary and you ask for the value of
 any function field for the first time.

5096. By Stefan Rijnhart (Opener)

[FIX] Don't preserve resource ids when clearing browse record cache, to
 prevent access errors after a refresh when the cache contains ids
 of unreadable records (for instance, of a parent company)
 Make accessing browse records robust against missing keys from
 the dictionary because of the first change.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'openerp/osv/orm.py'
--- openerp/osv/orm.py 2013-10-10 17:07:18 +0000
+++ openerp/osv/orm.py 2013-10-11 07:22:21 +0000
@@ -420,6 +420,7 @@
420 _logger.warning("No field_values found for ids %s in %s", ids, self)420 _logger.warning("No field_values found for ids %s in %s", ids, self)
421 raise KeyError('Field %s not found in %s'%(name, self))421 raise KeyError('Field %s not found in %s'%(name, self))
422 # create browse records for 'remote' objects422 # create browse records for 'remote' objects
423 new_m2o = {}
423 for result_line in field_values:424 for result_line in field_values:
424 new_data = {}425 new_data = {}
425 for field_name, field_column in fields_to_fetch:426 for field_name, field_column in fields_to_fetch:
@@ -448,6 +449,8 @@
448 context=self._context,449 context=self._context,
449 list_class=self._list_class,450 list_class=self._list_class,
450 fields_process=self._fields_process)451 fields_process=self._fields_process)
452 if value not in new_m2o.get(obj, []):
453 new_m2o.setdefault(obj, []).append(value)
451 else:454 else:
452 new_data[field_name] = value455 new_data[field_name] = value
453 else:456 else:
@@ -474,6 +477,15 @@
474 new_data[field_name] = result_line[field_name]477 new_data[field_name] = result_line[field_name]
475 self._data[result_line['id']].update(new_data)478 self._data[result_line['id']].update(new_data)
476479
480 if self._uid != SUPERUSER_ID:
481 for obj, obj_ids in new_m2o.iteritems():
482 accessible_ids = obj._search(
483 self._cr, self._uid, [('id', 'in', obj_ids)],
484 context=self._context, access_rights_uid=SUPERUSER_ID)
485 for obj_id in obj_ids:
486 if obj_id not in accessible_ids:
487 self._cache[obj._name].pop(obj_id)
488
477 if not name in self._data[self._id]:489 if not name in self._data[self._id]:
478 # How did this happen? Could be a missing model due to custom fields used too soon, see above.490 # How did this happen? Could be a missing model due to custom fields used too soon, see above.
479 self.__logger.error("Fields to fetch: %s, Field values: %s", field_names, field_values)491 self.__logger.error("Fields to fetch: %s, Field values: %s", field_names, field_values)