[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 logging
|
||||||
|
import operator
|
||||||
|
|
||||||
from inspect import currentframe, getargspec
|
from inspect import currentframe, getargspec
|
||||||
from collections import defaultdict, MutableMapping
|
from collections import defaultdict, MutableMapping
|
||||||
|
@ -685,7 +686,7 @@ class Environment(object):
|
||||||
yield
|
yield
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
cls._local.environments = WeakSet()
|
cls._local.environments = Environments()
|
||||||
yield
|
yield
|
||||||
finally:
|
finally:
|
||||||
release_local(cls._local)
|
release_local(cls._local)
|
||||||
|
@ -708,8 +709,6 @@ class Environment(object):
|
||||||
self.prefetch = defaultdict(set) # {model_name: set(id), ...}
|
self.prefetch = defaultdict(set) # {model_name: set(id), ...}
|
||||||
self.computed = defaultdict(set) # {field: set(id), ...}
|
self.computed = defaultdict(set) # {field: set(id), ...}
|
||||||
self.dirty = set() # set(record)
|
self.dirty = set() # set(record)
|
||||||
self.todo = {} # {field: records, ...}
|
|
||||||
self.mode = env.mode if env else Mode()
|
|
||||||
self.all = envs
|
self.all = envs
|
||||||
envs.add(self)
|
envs.add(self)
|
||||||
return self
|
return self
|
||||||
|
@ -746,14 +745,14 @@ class Environment(object):
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def _do_in_mode(self, mode):
|
def _do_in_mode(self, mode):
|
||||||
if self.mode.value:
|
if self.all.mode:
|
||||||
yield
|
yield
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
self.mode.value = mode
|
self.all.mode = mode
|
||||||
yield
|
yield
|
||||||
finally:
|
finally:
|
||||||
self.mode.value = False
|
self.all.mode = False
|
||||||
self.dirty.clear()
|
self.dirty.clear()
|
||||||
|
|
||||||
def do_in_draft(self):
|
def do_in_draft(self):
|
||||||
|
@ -765,7 +764,7 @@ class Environment(object):
|
||||||
@property
|
@property
|
||||||
def in_draft(self):
|
def in_draft(self):
|
||||||
""" Return whether we are in draft mode. """
|
""" Return whether we are in draft mode. """
|
||||||
return bool(self.mode.value)
|
return bool(self.all.mode)
|
||||||
|
|
||||||
def do_in_onchange(self):
|
def do_in_onchange(self):
|
||||||
""" Context-switch to 'onchange' draft mode, which is a specialized
|
""" Context-switch to 'onchange' draft mode, which is a specialized
|
||||||
|
@ -776,7 +775,7 @@ class Environment(object):
|
||||||
@property
|
@property
|
||||||
def in_onchange(self):
|
def in_onchange(self):
|
||||||
""" Return whether we are in 'onchange' draft mode. """
|
""" Return whether we are in 'onchange' draft mode. """
|
||||||
return self.mode.value == 'onchange'
|
return self.all.mode == 'onchange'
|
||||||
|
|
||||||
def invalidate(self, spec):
|
def invalidate(self, spec):
|
||||||
""" Invalidate some fields for some records in the cache of all
|
""" Invalidate some fields for some records in the cache of all
|
||||||
|
@ -788,7 +787,7 @@ class Environment(object):
|
||||||
"""
|
"""
|
||||||
if not spec:
|
if not spec:
|
||||||
return
|
return
|
||||||
for env in list(iter(self.all)):
|
for env in list(self.all):
|
||||||
c = env.cache
|
c = env.cache
|
||||||
for field, ids in spec:
|
for field, ids in spec:
|
||||||
if ids is None:
|
if ids is None:
|
||||||
|
@ -801,12 +800,49 @@ class Environment(object):
|
||||||
|
|
||||||
def invalidate_all(self):
|
def invalidate_all(self):
|
||||||
""" Clear the cache of all environments. """
|
""" Clear the cache of all environments. """
|
||||||
for env in list(iter(self.all)):
|
for env in list(self.all):
|
||||||
env.cache.clear()
|
env.cache.clear()
|
||||||
env.prefetch.clear()
|
env.prefetch.clear()
|
||||||
env.computed.clear()
|
env.computed.clear()
|
||||||
env.dirty.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):
|
def check_cache(self):
|
||||||
""" Check the cache consistency. """
|
""" Check the cache consistency. """
|
||||||
# make a full copy of the cache, and invalidate it
|
# 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))
|
raise Warning('Invalid cache for fields\n' + pformat(invalids))
|
||||||
|
|
||||||
|
|
||||||
class Mode(object):
|
class Environments(object):
|
||||||
""" A mode flag shared among environments. """
|
""" A common object for all environments in a request. """
|
||||||
value = False # False, True (draft) or 'onchange' (onchange draft)
|
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
|
# keep those imports here in order to handle cyclic dependencies correctly
|
||||||
|
|
|
@ -3149,9 +3149,9 @@ class BaseModel(object):
|
||||||
if self.env.in_draft:
|
if self.env.in_draft:
|
||||||
# we may be doing an onchange, do not prefetch other fields
|
# we may be doing an onchange, do not prefetch other fields
|
||||||
pass
|
pass
|
||||||
elif field in self.env.todo:
|
elif self.env.field_todo(field):
|
||||||
# field must be recomputed, do not prefetch records to recompute
|
# 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:
|
elif self._columns[field.name]._prefetch:
|
||||||
# here we can optimize: prefetch all classic and many2one fields
|
# here we can optimize: prefetch all classic and many2one fields
|
||||||
fnames = set(fname
|
fnames = set(fname
|
||||||
|
@ -5498,42 +5498,34 @@ class BaseModel(object):
|
||||||
""" If `field` must be recomputed on some record in `self`, return the
|
""" If `field` must be recomputed on some record in `self`, return the
|
||||||
corresponding records that must be recomputed.
|
corresponding records that must be recomputed.
|
||||||
"""
|
"""
|
||||||
for env in [self.env] + list(iter(self.env.all)):
|
return self.env.check_todo(field, self)
|
||||||
if env.todo.get(field) and env.todo[field] & self:
|
|
||||||
return env.todo[field]
|
|
||||||
|
|
||||||
def _recompute_todo(self, field):
|
def _recompute_todo(self, field):
|
||||||
""" Mark `field` to be recomputed. """
|
""" Mark `field` to be recomputed. """
|
||||||
todo = self.env.todo
|
self.env.add_todo(field, self)
|
||||||
todo[field] = (todo.get(field) or self.browse()) | self
|
|
||||||
|
|
||||||
def _recompute_done(self, field):
|
def _recompute_done(self, field):
|
||||||
""" Mark `field` as being recomputed. """
|
""" Mark `field` as recomputed. """
|
||||||
todo = self.env.todo
|
self.env.remove_todo(field, self)
|
||||||
if field in todo:
|
|
||||||
recs = todo.pop(field) - self
|
|
||||||
if recs:
|
|
||||||
todo[field] = recs
|
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def recompute(self):
|
def recompute(self):
|
||||||
""" Recompute stored function fields. The fields and records to
|
""" Recompute stored function fields. The fields and records to
|
||||||
recompute have been determined by method :meth:`modified`.
|
recompute have been determined by method :meth:`modified`.
|
||||||
"""
|
"""
|
||||||
for env in list(iter(self.env.all)):
|
while self.env.has_todo():
|
||||||
while env.todo:
|
field, recs = self.env.get_todo()
|
||||||
field, recs = next(env.todo.iteritems())
|
# evaluate the fields to recompute, and save them to database
|
||||||
# evaluate the fields to recompute, and save them to database
|
for rec, rec1 in zip(recs, recs.with_context(recompute=False)):
|
||||||
for rec, rec1 in zip(recs, recs.with_context(recompute=False)):
|
try:
|
||||||
try:
|
values = rec._convert_to_write({
|
||||||
values = rec._convert_to_write({
|
f.name: rec[f.name] for f in field.computed_fields
|
||||||
f.name: rec[f.name] for f in field.computed_fields
|
})
|
||||||
})
|
rec1._write(values)
|
||||||
rec1._write(values)
|
except MissingError:
|
||||||
except MissingError:
|
pass
|
||||||
pass
|
# mark the computed fields as done
|
||||||
# mark the computed fields as done
|
map(recs._recompute_done, field.computed_fields)
|
||||||
map(recs._recompute_done, field.computed_fields)
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Generic onchange method
|
# Generic onchange method
|
||||||
|
|
Loading…
Reference in New Issue