Commit 085a46be authored by Arnaud Fontaine's avatar Arnaud Fontaine

ERP5Form: Reset Form Field Value Cache on all nodes upon modification.

Before, the field value cache was only invalidated on the node where a field
has been modified, so similarly to reset of ZODB Components, implement
synchronization on all nodes through ZODB Cache Cookie.
parent 7a051860
...@@ -42,10 +42,6 @@ from hashlib import md5 ...@@ -42,10 +42,6 @@ from hashlib import md5
import time import time
from zope.interface import Interface from zope.interface import Interface
from zope.interface import implements from zope.interface import implements
_field_value_cache = {}
def purgeFieldValueCache():
_field_value_cache.clear()
class ICaptchaProvider(Interface): class ICaptchaProvider(Interface):
"""The CaptchaProvider interface provides a captcha generator.""" """The CaptchaProvider interface provides a captcha generator."""
......
...@@ -48,9 +48,38 @@ from Products.PageTemplates.Expressions import SecureModuleImporter ...@@ -48,9 +48,38 @@ from Products.PageTemplates.Expressions import SecureModuleImporter
from Products.ERP5Type.PsycoWrapper import psyco from Products.ERP5Type.PsycoWrapper import psyco
import sys import sys
_field_value_cache = {} class FieldValueCacheDict(dict):
def purgeFieldValueCache(): _last_sync = -1
_field_value_cache.clear()
def clear(self):
super(FieldValueCacheDict, self).clear()
from Products.ERP5.ERP5Site import getSite
try:
portal = getSite()
except IndexError:
pass
else:
portal.newCacheCookie('form_field_value_cache')
self._last_sync = portal.getCacheCookie('form_field_value_cache')
def __getitem__(self, cache_id):
from Products.ERP5.ERP5Site import getSite
try:
portal = getSite()
except IndexError:
pass
else:
cookie = portal.getCacheCookie('form_field_value_cache')
if cookie != self._last_sync:
LOG("ERP5Form.Form", 0, "Resetting form field value cache")
self._last_sync = cookie
self.clear()
raise KeyError('Field cache is outdated and has been reset')
return super(FieldValueCacheDict, self).__getitem__(cache_id)
field_value_cache = FieldValueCacheDict()
# Patch the fiels methods to provide improved namespace handling # Patch the fiels methods to provide improved namespace handling
...@@ -343,7 +372,7 @@ def get_value(self, id, REQUEST=None, **kw): ...@@ -343,7 +372,7 @@ def get_value(self, id, REQUEST=None, **kw):
id) id)
try: try:
value = _field_value_cache[cache_id] value = field_value_cache[cache_id]
except KeyError: except KeyError:
# either returns non callable value (ex. "Title") # either returns non callable value (ex. "Title")
# or a FieldValue instance of appropriate class # or a FieldValue instance of appropriate class
...@@ -353,7 +382,7 @@ def get_value(self, id, REQUEST=None, **kw): ...@@ -353,7 +382,7 @@ def get_value(self, id, REQUEST=None, **kw):
# and caching sometimes break these field settings at initialization. # and caching sometimes break these field settings at initialization.
# As the result, we would see broken field editing screen in ZMI. # As the result, we would see broken field editing screen in ZMI.
if cacheable and self._p_oid: if cacheable and self._p_oid:
_field_value_cache[cache_id] = value field_value_cache[cache_id] = value
if callable(value): if callable(value):
return value(field, id, **kw) return value(field, id, **kw)
......
...@@ -52,6 +52,5 @@ class FieldValueCacheInteractor(Interactor): ...@@ -52,6 +52,5 @@ class FieldValueCacheInteractor(Interactor):
Interaction method (defined at the Interactor level). Interaction method (defined at the Interactor level).
Make sure all field value caches are purged Make sure all field value caches are purged
""" """
from Products.ERP5Form import Form, ProxyField from Products.ERP5Form.Form import field_value_cache
Form.purgeFieldValueCache() field_value_cache.clear()
ProxyField.purgeFieldValueCache()
...@@ -60,10 +60,6 @@ from thread import get_ident ...@@ -60,10 +60,6 @@ from thread import get_ident
_USE_ORIGINAL_GET_VALUE_MARKER = [] _USE_ORIGINAL_GET_VALUE_MARKER = []
_field_value_cache = {}
def purgeFieldValueCache():
_field_value_cache.clear()
class WidgetDelegatedMethod(Method): class WidgetDelegatedMethod(Method):
"""Method delegated to the proxied field's widget. """Method delegated to the proxied field's widget.
""" """
...@@ -740,14 +736,15 @@ class ProxyField(ZMIField): ...@@ -740,14 +736,15 @@ class ProxyField(ZMIField):
field._p_oid, field._p_oid,
id) id)
from Products.ERP5Form.Form import field_value_cache
try: try:
value = _field_value_cache[cache_id] value = field_value_cache[cache_id]
except KeyError: except KeyError:
# either returns non callable value (ex. "Title") # either returns non callable value (ex. "Title")
# or a FieldValue instance of appropriate class # or a FieldValue instance of appropriate class
value, cacheable = self.getFieldValue(field, id, **kw) value, cacheable = self.getFieldValue(field, id, **kw)
if cacheable: if cacheable:
_field_value_cache[cache_id] = value field_value_cache[cache_id] = value
if value is _USE_ORIGINAL_GET_VALUE_MARKER: if value is _USE_ORIGINAL_GET_VALUE_MARKER:
return proxy_field.get_value(id, **kw) return proxy_field.get_value(id, **kw)
......
...@@ -70,23 +70,23 @@ class TestFieldValueCache(ERP5TypeTestCase): ...@@ -70,23 +70,23 @@ class TestFieldValueCache(ERP5TypeTestCase):
# Get form value # Get form value
field = form.my_first_name field = form.my_first_name
id = 'title' id = 'title'
from Products.ERP5Form.ProxyField import _field_value_cache from Products.ERP5Form.Form import field_value_cache
cache_id = ('ProxyField.get_value', cache_id = ('ProxyField.get_value',
field._p_oid, field._p_oid,
field._p_oid, field._p_oid,
id) id)
# Make sure cache has field # Make sure cache has field
self.assertTrue(_field_value_cache.has_key(cache_id)) self.assertTrue(field_value_cache.has_key(cache_id))
# Make sure cache and field are equal # Make sure cache and field are equal
self.assertEquals(field.get_value(id), _field_value_cache[cache_id]) self.assertEquals(field.get_value(id), field_value_cache[cache_id])
# Call manage_renameObject # Call manage_renameObject
form.manage_renameObject('my_first_name', 'my_first_name2') form.manage_renameObject('my_first_name', 'my_first_name2')
form.manage_renameObject('my_first_name2', 'my_first_name') form.manage_renameObject('my_first_name2', 'my_first_name')
# Make sure cache has no field # Make sure cache has no field
self.assertFalse(_field_value_cache.has_key(cache_id)) self.assertFalse(field_value_cache.has_key(cache_id))
# Render # Render
form() form()
# Make sure cache has field # Make sure cache has field
self.assertTrue(_field_value_cache.has_key(cache_id)) self.assertTrue(field_value_cache.has_key(cache_id))
# Make sure cache and field are equal # Make sure cache and field are equal
self.assertEquals(field.get_value(id), _field_value_cache[cache_id]) self.assertEquals(field.get_value(id), field_value_cache[cache_id])
...@@ -46,7 +46,7 @@ from Products.Formulator.TALESField import TALESMethod ...@@ -46,7 +46,7 @@ from Products.Formulator.TALESField import TALESMethod
from Products.ERP5Type.Core.Folder import Folder from Products.ERP5Type.Core.Folder import Folder
from Products.ERP5Form.Form import ERP5Form from Products.ERP5Form.Form import ERP5Form
from Products.ERP5Form.Form import purgeFieldValueCache from Products.ERP5Form.Form import field_value_cache
from Products.ERP5Form.Form import getFieldValue from Products.ERP5Form.Form import getFieldValue
from Products.ERP5Form import Form from Products.ERP5Form import Form
from Products.ERP5Form import ProxyField from Products.ERP5Form import ProxyField
...@@ -130,7 +130,7 @@ class TestFloatField(ERP5TypeTestCase): ...@@ -130,7 +130,7 @@ class TestFloatField(ERP5TypeTestCase):
# value is rounded # value is rounded
self.assertEquals('13', self.widget.format_value(self.field, 12.9)) self.assertEquals('13', self.widget.format_value(self.field, 12.9))
purgeFieldValueCache() # call this before changing internal field values. field_value_cache.clear() # call this before changing internal field values.
self.field.values['precision'] = 2 self.field.values['precision'] = 2
self.assertEquals('0.01', self.widget.format_value(self.field, 0.011)) self.assertEquals('0.01', self.widget.format_value(self.field, 0.011))
# value is rounded # value is rounded
...@@ -934,42 +934,50 @@ class TestFieldValueCache(ERP5TypeTestCase): ...@@ -934,42 +934,50 @@ class TestFieldValueCache(ERP5TypeTestCase):
self.assertEqual(False, value.value is field.values['external_validator']) self.assertEqual(False, value.value is field.values['external_validator'])
self.assertEqual(True, type(value.value) is Method) self.assertEqual(True, type(value.value) is Method)
def _getCacheSize(self, cache_id):
count = 0
for cache_key in field_value_cache.viewkeys():
if cache_key[0] == cache_id:
count += 1
return count
def test_using_cache_or_not(self): def test_using_cache_or_not(self):
# check standard field in zodb # check standard field in zodb
# make sure that this will use cache. # make sure that this will use cache.
cache_size = len(Form._field_value_cache) cache_size = self._getCacheSize('Form.get_value')
self.root.form.field.get_value('title') self.root.form.field.get_value('title')
self.assertEqual(True, cache_size < len(Form._field_value_cache)) self.assertEqual(True, cache_size < self._getCacheSize('Form.get_value'))
# check on-memory field # check on-memory field
# make sure that this will not use cache. # make sure that this will not use cache.
cache_size = len(Form._field_value_cache) cache_size = self._getCacheSize('Form.get_value')
self.assertEqual(repr(self.root), self.assertEqual(repr(self.root),
self.root.form.my_on_memory_tales_field.get_value('default')) self.root.form.my_on_memory_tales_field.get_value('default'))
self.assertEqual('123', self.assertEqual('123',
self.root.form.my_on_memory_field.get_value('default')) self.root.form.my_on_memory_field.get_value('default'))
self.assertEqual(True, cache_size == len(Form._field_value_cache)) self.assertEqual(True, cache_size == self._getCacheSize('Form.get_value'))
# check proxy field # check proxy field
# make sure that this will use cache. # make sure that this will use cache.
cache_size = len(ProxyField._field_value_cache) cache_size = self._getCacheSize('ProxyField.get_value')
self.root.form.proxy_field.get_value('title') self.root.form.proxy_field.get_value('title')
self.assertEqual(True, cache_size < len(ProxyField._field_value_cache)) self.assertEqual(True, cache_size < self._getCacheSize('ProxyField.get_value'))
# check proxy field with tales # check proxy field with tales
# make sure that this will not use cache. # make sure that this will not use cache.
cache_size = len(ProxyField._field_value_cache) cache_size = self._getCacheSize('ProxyField.get_value')
self.root.form.proxy_field_tales.get_value('title') self.root.form.proxy_field_tales.get_value('title')
self.assertEqual(True, cache_size == len(ProxyField._field_value_cache)) self.assertEqual(True, cache_size == self._getCacheSize('ProxyField.get_value'))
def test_datetime_field(self): def test_datetime_field(self):
purgeFieldValueCache() field_value_cache.clear()
# make sure that boundmethod must not be cached. # make sure that boundmethod must not be cached.
year_field = self.root.form.datetime_field.sub_form.get_field('year', include_disabled=1) year_field = self.root.form.datetime_field.sub_form.get_field('year', include_disabled=1)
self.assertEqual(True, type(year_field.overrides['items']) is BoundMethod) self.assertEqual(True, type(year_field.overrides['items']) is BoundMethod)
cache_size = len(Form._field_value_cache) cache_size = len(field_value_cache)
year_field.get_value('items') year_field.get_value('items')
# See Formulator/StandardFields.py(line:174) # See Formulator/StandardFields.py(line:174)
...@@ -981,22 +989,22 @@ class TestFieldValueCache(ERP5TypeTestCase): ...@@ -981,22 +989,22 @@ class TestFieldValueCache(ERP5TypeTestCase):
self.root.form.datetime_field._p_oid, self.root.form.datetime_field._p_oid,
self.root.form.datetime_field._p_oid, self.root.form.datetime_field._p_oid,
'start_datetime' 'start_datetime'
) in Form._field_value_cache) ) in field_value_cache)
self.assertEqual(True, ('Form.get_value', self.assertEqual(True, ('Form.get_value',
self.root.form.datetime_field._p_oid, self.root.form.datetime_field._p_oid,
self.root.form.datetime_field._p_oid, self.root.form.datetime_field._p_oid,
'end_datetime' 'end_datetime'
) in Form._field_value_cache) ) in field_value_cache)
self.assertEqual(False, ('Form.get_value', self.assertEqual(False, ('Form.get_value',
year_field._p_oid, year_field._p_oid,
year_field._p_oid, year_field._p_oid,
'items' 'items'
) in Form._field_value_cache) ) in field_value_cache)
self.assertEqual(cache_size, len(Form._field_value_cache)) self.assertEqual(cache_size, len(field_value_cache))
year_field.get_value('size') year_field.get_value('size')
year_field.get_value('default') year_field.get_value('default')
self.assertEqual(cache_size+2, len(Form._field_value_cache)) self.assertEqual(cache_size+2, len(field_value_cache))
def makeDummyOid(): def makeDummyOid():
import time, random import time, random
......
...@@ -30,8 +30,7 @@ from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase ...@@ -30,8 +30,7 @@ from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase
from Products.Formulator.TALESField import TALESMethod from Products.Formulator.TALESField import TALESMethod
from Products.ERP5Type.Core.Folder import Folder from Products.ERP5Type.Core.Folder import Folder
from Products.ERP5Form.Form import ERP5Form from Products.ERP5Form.Form import ERP5Form
from Products.ERP5Form.ProxyField import purgeFieldValueCache from Products.ERP5Form.Form import field_value_cache
class TestProxify(ERP5TypeTestCase): class TestProxify(ERP5TypeTestCase):
...@@ -103,12 +102,12 @@ class TestProxify(ERP5TypeTestCase): ...@@ -103,12 +102,12 @@ class TestProxify(ERP5TypeTestCase):
self.assertEqual(field.is_delegated('description'), True) self.assertEqual(field.is_delegated('description'), True)
self.assertEqual(field.get_value('description'), '') self.assertEqual(field.get_value('description'), '')
purgeFieldValueCache() # must purge cache before changing internal field value. field_value_cache.clear() # must purge cache before changing internal field value.
template_field = self.base_view.my_string_field template_field = self.base_view.my_string_field
template_field.values['description'] = 'Description' template_field.values['description'] = 'Description'
self.assertEqual(field.get_value('description'), 'Description') self.assertEqual(field.get_value('description'), 'Description')
purgeFieldValueCache() field_value_cache.clear()
# ListField # ListField
self.person_view.manage_addField('my_gender', 'Gender', 'ListField') self.person_view.manage_addField('my_gender', 'Gender', 'ListField')
...@@ -119,7 +118,7 @@ class TestProxify(ERP5TypeTestCase): ...@@ -119,7 +118,7 @@ class TestProxify(ERP5TypeTestCase):
self.assertEqual(field.is_delegated('items'), True) self.assertEqual(field.is_delegated('items'), True)
self.assertEqual(field.get_value('items'), [('Male', 'Male'), ('Female', 'Female')]) self.assertEqual(field.get_value('items'), [('Male', 'Male'), ('Female', 'Female')])
purgeFieldValueCache() field_value_cache.clear()
def test_multi_level_proxify(self): def test_multi_level_proxify(self):
...@@ -146,7 +145,7 @@ class TestProxify(ERP5TypeTestCase): ...@@ -146,7 +145,7 @@ class TestProxify(ERP5TypeTestCase):
self.assertEqual(field.has_value('scrap_variable'), 0) self.assertEqual(field.has_value('scrap_variable'), 0)
purgeFieldValueCache() # must purge cache before changing internal field value. field_value_cache.clear() # must purge cache before changing internal field value.
template_field = self.address_view.my_region template_field = self.address_view.my_region
template_field.values['title'] = 'Region' template_field.values['title'] = 'Region'
self.assertEqual(field.get_value('title'), 'Region') self.assertEqual(field.get_value('title'), 'Region')
...@@ -192,7 +191,7 @@ class TestProxify(ERP5TypeTestCase): ...@@ -192,7 +191,7 @@ class TestProxify(ERP5TypeTestCase):
#Proxify First #Proxify First
self.address_view.proxifyField({'my_region':'Base_view.my_list_field'}) self.address_view.proxifyField({'my_region':'Base_view.my_list_field'})
self.person_view.proxifyField({'my_default_region':'Address_view.my_region'}) self.person_view.proxifyField({'my_default_region':'Address_view.my_region'})
purgeFieldValueCache() field_value_cache.clear()
#UnProxify #UnProxify
self.person_view.unProxifyField({'my_default_region':'on'}) self.person_view.unProxifyField({'my_default_region':'on'})
field = self.person_view.my_default_region field = self.person_view.my_default_region
...@@ -204,7 +203,7 @@ class TestProxify(ERP5TypeTestCase): ...@@ -204,7 +203,7 @@ class TestProxify(ERP5TypeTestCase):
#Test unproxify with old instance. #Test unproxify with old instance.
#Proxify First #Proxify First
self.person_view.proxifyField({'my_career_subordination_title':'Base_view.my_relation_string_field'}) self.person_view.proxifyField({'my_career_subordination_title':'Base_view.my_relation_string_field'})
purgeFieldValueCache() field_value_cache.clear()
#UnProxify #UnProxify
self.person_view.unProxifyField({'my_career_subordination_title':'on'}) self.person_view.unProxifyField({'my_career_subordination_title':'on'})
field = self.person_view.my_career_subordination_title field = self.person_view.my_career_subordination_title
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment