Merge lp:~lifeless/launchpad/cachedproperty into lp:launchpad

Proposed by Robert Collins
Status: Merged
Approved by: Michael Hudson-Doyle
Approved revision: no longer in the source branch.
Merged at revision: 11366
Proposed branch: lp:~lifeless/launchpad/cachedproperty
Merge into: lp:launchpad
Diff against target: 419 lines (+179/-20)
9 files modified
lib/canonical/cachedproperty.py (+145/-4)
lib/canonical/database/sqlbase.py (+12/-0)
lib/canonical/launchpad/database/librarian.py (+1/-0)
lib/lp/registry/doc/teammembership.txt (+2/-1)
lib/lp/registry/model/distribution.py (+4/-3)
lib/lp/registry/model/person.py (+10/-10)
lib/lp/registry/model/product.py (+1/-2)
lib/lp/translations/model/potemplate.py (+1/-0)
lib/lp/translations/model/potmsgset.py (+3/-0)
To merge this branch: bzr merge lp:~lifeless/launchpad/cachedproperty
Reviewer Review Type Date Requested Status
Michael Hudson-Doyle Approve
Review via email: mp+32728@code.launchpad.net

Commit message

Automatically clear cachedproperties on SQLBase derived classes when storm invalidates an object.

Description of the change

Make invalidation of cached properties on SQLBase derived classes automatic. This addresses many of the risks associated with using them in the test suite (the server already discards all such objects between transactions), leaving just read-write-readback cases to handle as and when they occur.

To post a comment you must log in.
Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :

Hi,

This looks good. I'm not sure what value there is in initializing _cached_properties in SQLBase.__init__ is -- seems to slightly increase coupling.

Generally, yay for safer-by-default.

Cheers,
mwh

review: Approve
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

Is there or will there be a similar mechanism for pure Storm classes? Otherwise this adds a nasty pitfall to Storm migration.

Revision history for this message
Robert Collins (lifeless) wrote :

On Mon, Aug 16, 2010 at 3:51 PM, Jeroen T. Vermeulen <email address hidden> wrote:
> Is there or will there be a similar mechanism for pure Storm classes?  Otherwise this adds a nasty pitfall to Storm migration.

Its a two liner:

def __storm_invalidate(self):
    clear_cachedproperties(self)

SQLBase is storm to all intents and purposes these days.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/canonical/cachedproperty.py'
--- lib/canonical/cachedproperty.py 2010-01-11 21:29:33 +0000
+++ lib/canonical/cachedproperty.py 2010-08-17 01:31:02 +0000
@@ -3,10 +3,24 @@
33
4"""Cached properties for situations where a property is computed once and4"""Cached properties for situations where a property is computed once and
5then returned each time it is asked for.5then returned each time it is asked for.
6
7The clear_cachedproperties function can be used to wipe the cache of properties
8from an instance.
6"""9"""
710
8__metaclass__ = type11__metaclass__ = type
912
13__all__ = [
14 'cache_property',
15 'cachedproperty',
16 'clear_cachedproperties',
17 'clear_property',
18 ]
19
20from zope.security.proxy import removeSecurityProxy
21
22from canonical.lazr.utils import safe_hasattr
23
10# XXX: JonathanLange 2010-01-11 bug=505731: Move this to lp.services.24# XXX: JonathanLange 2010-01-11 bug=505731: Move this to lp.services.
1125
12def cachedproperty(attrname_or_fn):26def cachedproperty(attrname_or_fn):
@@ -17,6 +31,10 @@
1731
18 If you don't provide a name, the mangled name of the property is used.32 If you don't provide a name, the mangled name of the property is used.
1933
34 cachedproperty is not threadsafe - it should not be used on objects which
35 are shared across threads / external locking should be used on those
36 objects.
37
20 >>> class CachedPropertyTest(object):38 >>> class CachedPropertyTest(object):
21 ...39 ...
22 ... @cachedproperty('_foo_cache')40 ... @cachedproperty('_foo_cache')
@@ -44,6 +62,10 @@
44 6962 69
45 >>> cpt._bar_cached_value63 >>> cpt._bar_cached_value
46 6964 69
65
66 Cached properties are listed on instances.
67 >>> sorted(cpt._cached_properties)
68 ['_bar_cached_value', '_foo_cache']
4769
48 """70 """
49 if isinstance(attrname_or_fn, basestring):71 if isinstance(attrname_or_fn, basestring):
@@ -54,8 +76,125 @@
54 attrname = '_%s_cached_value' % fn.__name__76 attrname = '_%s_cached_value' % fn.__name__
55 return CachedProperty(attrname, fn)77 return CachedProperty(attrname, fn)
5678
79def cache_property(instance, attrname, value):
80 """Cache value on instance as attrname.
81
82 instance._cached_properties is updated with attrname.
83
84 >>> class CachedPropertyTest(object):
85 ...
86 ... @cachedproperty('_foo_cache')
87 ... def foo(self):
88 ... return 23
89 ...
90 >>> instance = CachedPropertyTest()
91 >>> cache_property(instance, '_foo_cache', 42)
92 >>> instance.foo
93 42
94 >>> instance._cached_properties
95 ['_foo_cache']
96
97 Caching a new value does not duplicate the cache keys.
98
99 >>> cache_property(instance, '_foo_cache', 84)
100 >>> instance._cached_properties
101 ['_foo_cache']
102
103 And does update the cached value.
104
105 >>> instance.foo
106 84
107 """
108 naked_instance = removeSecurityProxy(instance)
109 clear_property(naked_instance, attrname)
110 setattr(naked_instance, attrname, value)
111 cached_properties = getattr(naked_instance, '_cached_properties', [])
112 cached_properties.append(attrname)
113 naked_instance._cached_properties = cached_properties
114
115
116def clear_property(instance, attrname):
117 """Remove a cached attribute from instance.
118
119 The attribute name is removed from instance._cached_properties.
120
121 If the property is not cached, nothing happens.
122
123 :seealso clear_cachedproperties: For clearing all cached items at once.
124
125 >>> class CachedPropertyTest(object):
126 ...
127 ... @cachedproperty('_foo_cache')
128 ... def foo(self):
129 ... return 23
130 ...
131 >>> instance = CachedPropertyTest()
132 >>> instance.foo
133 23
134 >>> clear_property(instance, '_foo_cache')
135 >>> instance._cached_properties
136 []
137 >>> is_cached(instance, '_foo_cache')
138 False
139 >>> clear_property(instance, '_foo_cache')
140 """
141 naked_instance = removeSecurityProxy(instance)
142 if not is_cached(naked_instance, attrname):
143 return
144 delattr(naked_instance, attrname)
145 naked_instance._cached_properties.remove(attrname)
146
147
148def clear_cachedproperties(instance):
149 """Clear cached properties from an object.
150
151 >>> class CachedPropertyTest(object):
152 ...
153 ... @cachedproperty('_foo_cache')
154 ... def foo(self):
155 ... return 23
156 ...
157 >>> instance = CachedPropertyTest()
158 >>> instance.foo
159 23
160 >>> instance._cached_properties
161 ['_foo_cache']
162 >>> clear_cachedproperties(instance)
163 >>> instance._cached_properties
164 []
165 >>> hasattr(instance, '_foo_cache')
166 False
167 """
168 naked_instance = removeSecurityProxy(instance)
169 cached_properties = getattr(naked_instance, '_cached_properties', [])
170 for property_name in cached_properties:
171 delattr(naked_instance, property_name)
172 naked_instance._cached_properties = []
173
174
175def is_cached(instance, attrname):
176 """Return True if attrname is cached on instance.
177
178 >>> class CachedPropertyTest(object):
179 ...
180 ... @cachedproperty('_foo_cache')
181 ... def foo(self):
182 ... return 23
183 ...
184 >>> instance = CachedPropertyTest()
185 >>> instance.foo
186 23
187 >>> is_cached(instance, '_foo_cache')
188 True
189 >>> is_cached(instance, '_var_cache')
190 False
191 """
192 naked_instance = removeSecurityProxy(instance)
193 return safe_hasattr(naked_instance, attrname)
194
57195
58class CachedPropertyForAttr:196class CachedPropertyForAttr:
197 """Curry a decorator to provide arguments to the CachedProperty."""
59198
60 def __init__(self, attrname):199 def __init__(self, attrname):
61 self.attrname = attrname200 self.attrname = attrname
@@ -66,18 +205,20 @@
66205
67class CachedProperty:206class CachedProperty:
68207
208 # Used to detect not-yet-cached properties.
209 sentinel = object()
210
69 def __init__(self, attrname, fn):211 def __init__(self, attrname, fn):
70 self.fn = fn212 self.fn = fn
71 self.attrname = attrname213 self.attrname = attrname
72 self.marker = object()
73214
74 def __get__(self, inst, cls=None):215 def __get__(self, inst, cls=None):
75 if inst is None:216 if inst is None:
76 return self217 return self
77 cachedresult = getattr(inst, self.attrname, self.marker)218 cachedresult = getattr(inst, self.attrname, CachedProperty.sentinel)
78 if cachedresult is self.marker:219 if cachedresult is CachedProperty.sentinel:
79 result = self.fn(inst)220 result = self.fn(inst)
80 setattr(inst, self.attrname, result)221 cache_property(inst, self.attrname, result)
81 return result222 return result
82 else:223 else:
83 return cachedresult224 return cachedresult
84225
=== modified file 'lib/canonical/database/sqlbase.py'
--- lib/canonical/database/sqlbase.py 2010-05-21 13:08:18 +0000
+++ lib/canonical/database/sqlbase.py 2010-08-17 01:31:02 +0000
@@ -30,6 +30,7 @@
3030
31from lazr.restful.interfaces import IRepresentationCache31from lazr.restful.interfaces import IRepresentationCache
3232
33from canonical.cachedproperty import clear_cachedproperties
33from canonical.config import config, dbconfig34from canonical.config import config, dbconfig
34from canonical.database.interfaces import ISQLBase35from canonical.database.interfaces import ISQLBase
3536
@@ -175,6 +176,9 @@
175 correct master Store.176 correct master Store.
176 """177 """
177 from canonical.launchpad.interfaces import IMasterStore178 from canonical.launchpad.interfaces import IMasterStore
179 # Make it simple to write dumb-invalidators - initialised
180 # _cached_properties to a valid list rather than just-in-time creation.
181 self._cached_properties = []
178 store = IMasterStore(self.__class__)182 store = IMasterStore(self.__class__)
179183
180 # The constructor will fail if objects from a different Store184 # The constructor will fail if objects from a different Store
@@ -253,6 +257,14 @@
253 cache = getUtility(IRepresentationCache)257 cache = getUtility(IRepresentationCache)
254 cache.delete(self)258 cache.delete(self)
255259
260 def __storm_invalidated__(self):
261 """Flush cached properties."""
262 # Note this is not directly tested; but the entire test suite blows up
263 # awesomely if its broken : its entirely unclear where tests for this
264 # should be -- RBC 20100816.
265 clear_cachedproperties(self)
266
267
256alreadyInstalledMsg = ("A ZopelessTransactionManager with these settings is "268alreadyInstalledMsg = ("A ZopelessTransactionManager with these settings is "
257"already installed. This is probably caused by calling initZopeless twice.")269"already installed. This is probably caused by calling initZopeless twice.")
258270
259271
=== modified file 'lib/canonical/launchpad/database/librarian.py'
--- lib/canonical/launchpad/database/librarian.py 2010-07-06 16:12:46 +0000
+++ lib/canonical/launchpad/database/librarian.py 2010-08-17 01:31:02 +0000
@@ -194,6 +194,7 @@
194194
195 def __storm_invalidated__(self):195 def __storm_invalidated__(self):
196 """Make sure that the file is closed across transaction boundary."""196 """Make sure that the file is closed across transaction boundary."""
197 super(LibraryFileAlias, self).__storm_invalidated__()
197 self.close()198 self.close()
198199
199200
200201
=== modified file 'lib/lp/registry/doc/teammembership.txt'
--- lib/lp/registry/doc/teammembership.txt 2010-07-14 15:28:42 +0000
+++ lib/lp/registry/doc/teammembership.txt 2010-08-17 01:31:02 +0000
@@ -1007,7 +1007,8 @@
1007 >>> from canonical.launchpad.interfaces import IMasterObject1007 >>> from canonical.launchpad.interfaces import IMasterObject
1008 >>> IMasterObject(bad_user.account).status = AccountStatus.SUSPENDED1008 >>> IMasterObject(bad_user.account).status = AccountStatus.SUSPENDED
1009 >>> IMasterObject(bad_user.preferredemail).status = EmailAddressStatus.OLD1009 >>> IMasterObject(bad_user.preferredemail).status = EmailAddressStatus.OLD
1010 >>> removeSecurityProxy(bad_user)._preferredemail_cached = None1010 >>> from canonical.cachedproperty import clear_property
1011 >>> clear_property(removeSecurityProxy(bad_user), '_preferredemail_cached')
1011 >>> transaction.commit()1012 >>> transaction.commit()
10121013
1013 >>> [m.displayname for m in t3.allmembers]1014 >>> [m.displayname for m in t3.allmembers]
10141015
=== modified file 'lib/lp/registry/model/distribution.py'
--- lib/lp/registry/model/distribution.py 2010-08-11 02:17:14 +0000
+++ lib/lp/registry/model/distribution.py 2010-08-17 01:31:02 +0000
@@ -19,7 +19,7 @@
19from zope.interface import alsoProvides, implements19from zope.interface import alsoProvides, implements
2020
21from lp.archivepublisher.debversion import Version21from lp.archivepublisher.debversion import Version
22from canonical.cachedproperty import cachedproperty22from canonical.cachedproperty import cachedproperty, clear_property
23from canonical.database.constants import UTC_NOW23from canonical.database.constants import UTC_NOW
2424
25from canonical.database.datetimecol import UtcDateTimeCol25from canonical.database.datetimecol import UtcDateTimeCol
@@ -1617,8 +1617,9 @@
1617 # This driver is a release manager.1617 # This driver is a release manager.
1618 series.driver = owner1618 series.driver = owner
16191619
1620 if safe_hasattr(self, '_cached_series'):1620 # May wish to add this to the series rather than clearing the cache --
1621 del self._cached_series1621 # RBC 20100816.
1622 clear_property(self, '_cached_series')
1622 return series1623 return series
16231624
1624 @property1625 @property
16251626
=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py 2010-08-13 08:26:15 +0000
+++ lib/lp/registry/model/person.py 2010-08-17 01:31:02 +0000
@@ -59,7 +59,7 @@
59from canonical.database.sqlbase import (59from canonical.database.sqlbase import (
60 cursor, quote, quote_like, sqlvalues, SQLBase)60 cursor, quote, quote_like, sqlvalues, SQLBase)
6161
62from canonical.cachedproperty import cachedproperty62from canonical.cachedproperty import cachedproperty, cache_property, clear_property
6363
64from canonical.lazr.utils import get_current_browser_request, safe_hasattr64from canonical.lazr.utils import get_current_browser_request, safe_hasattr
6565
@@ -390,13 +390,12 @@
390390
391 Order them by name if necessary.391 Order them by name if necessary.
392 """392 """
393 self._languages_cache = sorted(393 cache_property(self, '_languages_cache', sorted(
394 languages, key=attrgetter('englishname'))394 languages, key=attrgetter('englishname')))
395395
396 def deleteLanguagesCache(self):396 def deleteLanguagesCache(self):
397 """Delete this person's cached languages, if it exists."""397 """Delete this person's cached languages, if it exists."""
398 if safe_hasattr(self, '_languages_cache'):398 clear_property(self, '_languages_cache')
399 del self._languages_cache
400399
401 def addLanguage(self, language):400 def addLanguage(self, language):
402 """See `IPerson`."""401 """See `IPerson`."""
@@ -1462,7 +1461,7 @@
1462 include_teams=include_teams)1461 include_teams=include_teams)
1463 person_list = []1462 person_list = []
1464 for person, email in result:1463 for person, email in result:
1465 person._preferredemail_cached = email1464 cache_property(person, '_preferredemail_cached', email)
1466 person_list.append(person)1465 person_list.append(person)
1467 return person_list1466 return person_list
14681467
@@ -1737,7 +1736,7 @@
1737 self.account_status = AccountStatus.DEACTIVATED1736 self.account_status = AccountStatus.DEACTIVATED
1738 self.account_status_comment = comment1737 self.account_status_comment = comment
1739 IMasterObject(self.preferredemail).status = EmailAddressStatus.NEW1738 IMasterObject(self.preferredemail).status = EmailAddressStatus.NEW
1740 self._preferredemail_cached = None1739 clear_property(self, '_preferredemail_cached')
1741 base_new_name = self.name + '-deactivatedaccount'1740 base_new_name = self.name + '-deactivatedaccount'
1742 self.name = self._ensureNewName(base_new_name)1741 self.name = self._ensureNewName(base_new_name)
17431742
@@ -2070,7 +2069,7 @@
2070 if email_address is not None:2069 if email_address is not None:
2071 email_address.status = EmailAddressStatus.VALIDATED2070 email_address.status = EmailAddressStatus.VALIDATED
2072 email_address.syncUpdate()2071 email_address.syncUpdate()
2073 self._preferredemail_cached = None2072 clear_property(self, '_preferredemail_cached')
20742073
2075 def setPreferredEmail(self, email):2074 def setPreferredEmail(self, email):
2076 """See `IPerson`."""2075 """See `IPerson`."""
@@ -2107,7 +2106,7 @@
2107 IMasterObject(email).syncUpdate()2106 IMasterObject(email).syncUpdate()
21082107
2109 # Now we update our cache of the preferredemail.2108 # Now we update our cache of the preferredemail.
2110 self._preferredemail_cached = email2109 cache_property(self, '_preferredemail_cached', email)
21112110
2112 @cachedproperty('_preferredemail_cached')2111 @cachedproperty('_preferredemail_cached')
2113 def preferredemail(self):2112 def preferredemail(self):
@@ -2622,7 +2621,8 @@
2622 # Populate the previously empty 'preferredemail' cached2621 # Populate the previously empty 'preferredemail' cached
2623 # property, so the Person record is up-to-date.2622 # property, so the Person record is up-to-date.
2624 if master_email.status == EmailAddressStatus.PREFERRED:2623 if master_email.status == EmailAddressStatus.PREFERRED:
2625 account_person._preferredemail_cached = master_email2624 cache_property(account_person, '_preferredemail_cached',
2625 master_email)
2626 return account_person2626 return account_person
2627 # There is no associated `Person` to the email `Account`.2627 # There is no associated `Person` to the email `Account`.
2628 # This is probably because the account was created externally2628 # This is probably because the account was created externally
26292629
=== modified file 'lib/lp/registry/model/product.py'
--- lib/lp/registry/model/product.py 2010-08-06 14:49:35 +0000
+++ lib/lp/registry/model/product.py 2010-08-17 01:31:02 +0000
@@ -509,9 +509,8 @@
509509
510 def __storm_invalidated__(self):510 def __storm_invalidated__(self):
511 """Clear cached non-storm attributes when the transaction ends."""511 """Clear cached non-storm attributes when the transaction ends."""
512 super(Product, self).__storm_invalidated__()
512 self._cached_licenses = None513 self._cached_licenses = None
513 if safe_hasattr(self, '_commercial_subscription_cached'):
514 del self._commercial_subscription_cached
515514
516 def _getLicenses(self):515 def _getLicenses(self):
517 """Get the licenses as a tuple."""516 """Get the licenses as a tuple."""
518517
=== modified file 'lib/lp/translations/model/potemplate.py'
--- lib/lp/translations/model/potemplate.py 2010-08-06 09:32:02 +0000
+++ lib/lp/translations/model/potemplate.py 2010-08-17 01:31:02 +0000
@@ -185,6 +185,7 @@
185 _uses_english_msgids = None185 _uses_english_msgids = None
186186
187 def __storm_invalidated__(self):187 def __storm_invalidated__(self):
188 super(POTemplate, self).__storm_invalidated__()
188 self.clearPOFileCache()189 self.clearPOFileCache()
189 self._uses_english_msgids = None190 self._uses_english_msgids = None
190191
191192
=== modified file 'lib/lp/translations/model/potmsgset.py'
--- lib/lp/translations/model/potmsgset.py 2010-08-06 06:51:37 +0000
+++ lib/lp/translations/model/potmsgset.py 2010-08-17 01:31:02 +0000
@@ -103,6 +103,7 @@
103 credits_message_ids = credits_message_info.keys()103 credits_message_ids = credits_message_info.keys()
104104
105 def __storm_invalidated__(self):105 def __storm_invalidated__(self):
106 super(POTMsgSet, self).__storm_invalidated__()
106 self._cached_singular_text = None107 self._cached_singular_text = None
107 self._cached_uses_english_msgids = None108 self._cached_uses_english_msgids = None
108109
@@ -157,6 +158,7 @@
157 @property158 @property
158 def uses_english_msgids(self):159 def uses_english_msgids(self):
159 """See `IPOTMsgSet`."""160 """See `IPOTMsgSet`."""
161 # TODO: convert to cachedproperty, it will be simpler.
160 if self._cached_uses_english_msgids is not None:162 if self._cached_uses_english_msgids is not None:
161 return self._cached_uses_english_msgids163 return self._cached_uses_english_msgids
162164
@@ -181,6 +183,7 @@
181 @property183 @property
182 def singular_text(self):184 def singular_text(self):
183 """See `IPOTMsgSet`."""185 """See `IPOTMsgSet`."""
186 # TODO: convert to cachedproperty, it will be simpler.
184 if self._cached_singular_text is not None:187 if self._cached_singular_text is not None:
185 return self._cached_singular_text188 return self._cached_singular_text
186189