[FIX] openerp.api.Environment: move recomputation todos into a shared object
This fixes a bug which is usually triggered in module account_followup, but does not occur deterministically. Some recomputations of computed fields are apparently missing. Environment objects containing recomputations todos and kept alive by a WeakSet, are removed by the Python garbage collector before recomputation takes place. We fix the bug by moving the recomputation todos in a non-weakref'ed object.
This commit is contained in:
parent
9a3b48de0f
commit
4ce06c238b
|
@ -59,6 +59,7 @@ __all__ = [
|
|||
]
|
||||
|
||||
import logging
|
||||
import operator
|
||||
|
||||
from inspect import currentframe, getargspec
|
||||
from collections import defaultdict, MutableMapping
|
||||
|
@ -685,7 +686,7 @@ class Environment(object):
|
|||
yield
|
||||
else:
|
||||
try:
|
||||
cls._local.environments = WeakSet()
|
||||
cls._local.environments = Environments()
|
||||
yield
|
||||
finally:
|
||||
release_local(cls._local)
|
||||
|
@ -708,8 +709,6 @@ class Environment(object):
|
|||
self.prefetch = defaultdict(set) # {model_name: set(id), ...}
|
||||
self.computed = defaultdict(set) # {field: set(id), ...}
|
||||
self.dirty = set() # set(record)
|
||||
self.todo = {} # {field: records, ...}
|
||||
self.mode = env.mode if env else Mode()
|
||||
self.all = envs
|
||||
envs.add(self)
|
||||
return self
|
||||
|
@ -746,14 +745,14 @@ class Environment(object):
|
|||
|
||||
@contextmanager
|
||||
def _do_in_mode(self, mode):
|
||||
if self.mode.value:
|
||||
if self.all.mode:
|
||||
yield
|
||||
else:
|
||||
try:
|
||||
self.mode.value = mode
|
||||
self.all.mode = mode
|
||||
yield
|
||||
finally:
|
||||
self.mode.value = False
|
||||
self.all.mode = False
|
||||
self.dirty.clear()
|
||||
|
||||
def do_in_draft(self):
|
||||
|
@ -765,7 +764,7 @@ class Environment(object):
|
|||
@property
|
||||
def in_draft(self):
|
||||
""" Return whether we are in draft mode. """
|
||||
return bool(self.mode.value)
|
||||
return bool(self.all.mode)
|
||||
|
||||
def do_in_onchange(self):
|
||||
""" Context-switch to 'onchange' draft mode, which is a specialized
|
||||
|
@ -776,7 +775,7 @@ class Environment(object):
|
|||
@property
|
||||
def in_onchange(self):
|
||||
""" Return whether we are in 'onchange' draft mode. """
|
||||
return self.mode.value == 'onchange'
|
||||
return self.all.mode == 'onchange'
|
||||
|
||||
def invalidate(self, spec):
|
||||
""" Invalidate some fields for some records in the cache of all
|
||||
|
@ -788,7 +787,7 @@ class Environment(object):
|
|||
"""
|
||||
if not spec:
|
||||
return
|
||||
for env in list(iter(self.all)):
|
||||
for env in list(self.all):
|
||||
c = env.cache
|
||||
for field, ids in spec:
|
||||
if ids is None:
|
||||
|
@ -801,12 +800,49 @@ class Environment(object):
|
|||
|
||||
def invalidate_all(self):
|
||||
""" Clear the cache of all environments. """
|
||||
for env in list(iter(self.all)):
|
||||
for env in list(self.all):
|
||||
env.cache.clear()
|
||||
env.prefetch.clear()
|
||||
env.computed.clear()
|
||||
env.dirty.clear()
|
||||
|
||||
def field_todo(self, field):
|
||||
""" Check whether `field` must be recomputed, and returns a recordset
|
||||
with all records to recompute for `field`.
|
||||
"""
|
||||
if field in self.all.todo:
|
||||
return reduce(operator.or_, self.all.todo[field])
|
||||
|
||||
def check_todo(self, field, record):
|
||||
""" Check whether `field` must be recomputed on `record`, and if so,
|
||||
returns the corresponding recordset to recompute.
|
||||
"""
|
||||
for recs in self.all.todo.get(field, []):
|
||||
if recs & record:
|
||||
return recs
|
||||
|
||||
def add_todo(self, field, records):
|
||||
""" Mark `field` to be recomputed on `records`. """
|
||||
recs_list = self.all.todo.setdefault(field, [])
|
||||
recs_list.append(records)
|
||||
|
||||
def remove_todo(self, field, records):
|
||||
""" Mark `field` as recomputed on `records`. """
|
||||
recs_list = self.all.todo.get(field, [])
|
||||
if records in recs_list:
|
||||
recs_list.remove(records)
|
||||
if not recs_list:
|
||||
del self.all.todo[field]
|
||||
|
||||
def has_todo(self):
|
||||
""" Return whether some fields must be recomputed. """
|
||||
return bool(self.all.todo)
|
||||
|
||||
def get_todo(self):
|
||||
""" Return a pair `(field, records)` to recompute. """
|
||||
for field, recs_list in self.all.todo.iteritems():
|
||||
return field, recs_list[0]
|
||||
|
||||
def check_cache(self):
|
||||
""" Check the cache consistency. """
|
||||
# make a full copy of the cache, and invalidate it
|
||||
|
@ -835,9 +871,20 @@ class Environment(object):
|
|||
raise Warning('Invalid cache for fields\n' + pformat(invalids))
|
||||
|
||||
|
||||
class Mode(object):
|
||||
""" A mode flag shared among environments. """
|
||||
value = False # False, True (draft) or 'onchange' (onchange draft)
|
||||
class Environments(object):
|
||||
""" A common object for all environments in a request. """
|
||||
def __init__(self):
|
||||
self.envs = WeakSet() # weak set of environments
|
||||
self.todo = {} # recomputations {field: [records]}
|
||||
self.mode = False # flag for draft/onchange
|
||||
|
||||
def add(self, env):
|
||||
""" Add the environment `env`. """
|
||||
self.envs.add(env)
|
||||
|
||||
def __iter__(self):
|
||||
""" Iterate over environments. """
|
||||
return iter(self.envs)
|
||||
|
||||
|
||||
# keep those imports here in order to handle cyclic dependencies correctly
|
||||
|
|
|
@ -3149,9 +3149,9 @@ class BaseModel(object):
|
|||
if self.env.in_draft:
|
||||
# we may be doing an onchange, do not prefetch other fields
|
||||
pass
|
||||
elif field in self.env.todo:
|
||||
elif self.env.field_todo(field):
|
||||
# field must be recomputed, do not prefetch records to recompute
|
||||
records -= self.env.todo[field]
|
||||
records -= self.env.field_todo(field)
|
||||
elif self._columns[field.name]._prefetch:
|
||||
# here we can optimize: prefetch all classic and many2one fields
|
||||
fnames = set(fname
|
||||
|
@ -5498,42 +5498,34 @@ class BaseModel(object):
|
|||
""" If `field` must be recomputed on some record in `self`, return the
|
||||
corresponding records that must be recomputed.
|
||||
"""
|
||||
for env in [self.env] + list(iter(self.env.all)):
|
||||
if env.todo.get(field) and env.todo[field] & self:
|
||||
return env.todo[field]
|
||||
return self.env.check_todo(field, self)
|
||||
|
||||
def _recompute_todo(self, field):
|
||||
""" Mark `field` to be recomputed. """
|
||||
todo = self.env.todo
|
||||
todo[field] = (todo.get(field) or self.browse()) | self
|
||||
self.env.add_todo(field, self)
|
||||
|
||||
def _recompute_done(self, field):
|
||||
""" Mark `field` as being recomputed. """
|
||||
todo = self.env.todo
|
||||
if field in todo:
|
||||
recs = todo.pop(field) - self
|
||||
if recs:
|
||||
todo[field] = recs
|
||||
""" Mark `field` as recomputed. """
|
||||
self.env.remove_todo(field, self)
|
||||
|
||||
@api.model
|
||||
def recompute(self):
|
||||
""" Recompute stored function fields. The fields and records to
|
||||
recompute have been determined by method :meth:`modified`.
|
||||
"""
|
||||
for env in list(iter(self.env.all)):
|
||||
while env.todo:
|
||||
field, recs = next(env.todo.iteritems())
|
||||
# evaluate the fields to recompute, and save them to database
|
||||
for rec, rec1 in zip(recs, recs.with_context(recompute=False)):
|
||||
try:
|
||||
values = rec._convert_to_write({
|
||||
f.name: rec[f.name] for f in field.computed_fields
|
||||
})
|
||||
rec1._write(values)
|
||||
except MissingError:
|
||||
pass
|
||||
# mark the computed fields as done
|
||||
map(recs._recompute_done, field.computed_fields)
|
||||
while self.env.has_todo():
|
||||
field, recs = self.env.get_todo()
|
||||
# evaluate the fields to recompute, and save them to database
|
||||
for rec, rec1 in zip(recs, recs.with_context(recompute=False)):
|
||||
try:
|
||||
values = rec._convert_to_write({
|
||||
f.name: rec[f.name] for f in field.computed_fields
|
||||
})
|
||||
rec1._write(values)
|
||||
except MissingError:
|
||||
pass
|
||||
# mark the computed fields as done
|
||||
map(recs._recompute_done, field.computed_fields)
|
||||
|
||||
#
|
||||
# Generic onchange method
|
||||
|
|
Loading…
Reference in New Issue