Merge lp:~leonardr/lazr.restful/representation_cache into lp:lazr.restful

Proposed by Leonard Richardson
Status: Merged
Approved by: Guilherme Salgado
Approved revision: 34
Merged at revision: not available
Proposed branch: lp:~leonardr/lazr.restful/representation_cache
Merge into: lp:lazr.restful
Diff against target: None lines
To merge this branch: bzr merge lp:~leonardr/lazr.restful/representation_cache
Reviewer Review Type Date Requested Status
Guilherme Salgado (community) Approve
Review via email: mp+5870@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Leonard Richardson (leonardr) wrote :

Test: bin/test
Diff: https://pastebin.canonical.com/16818/

This branch fixes a lazr.restful bug that was manifesting in Launchpad
as bug 336866: sending 412 ("Precondition Failed") to people who
messed with bugs.

The problem was that IBug.date_last_modified was being modified behind
the scened by an event listener. The old value for date_last_modified
wasn't removed from the cache of field representations, and so it got
reused when we sent the representation in response to the PATCH
request. We sent a transitional representation, containing all the changes
the client had made, but with the old value for date_last_modified.

The ETag we calculated for that representation was also
transitional. When the client used it on its next request, the
transitional ETag was compared against the ETag for the fullly new
representation (including the change to date_last_modified), and the
result was a 412 error.

date_last_modified is changed by an ObjectModifiedEvent listener. The
listeners run synchronously, so by the time control returns to
lazr.restful, the field has been changed. The trick is figuring out
which fields were changed.

This branch fixes the problem by changing the way the cache is
invalidated. Instead of iterating over the changeset, we iterate over
the cache. We have a snapshot of the object before it was changed, and
for each field in the cache, we compare the old object's value for
that field to the new object's value. If they're different, we clear
that value from the cache.

To properly test the branch, I introduced a date_last_modified-like field
to the sample web service. It's updated behind the scenes every time a
client changes a cookbook.

I had to tweak the tests a little bit because they were constantly sending PATCH requests that didn't change anything and assuming that the old ETags were still valid because nothing changed. Now that revision_number is updated every time you make a PATCH request, you can't reuse ETags.

Revision history for this message
Guilherme Salgado (salgado) wrote :

Hi Leonard,

This is a very nice and clean branch. Just one suggestion below.

 review approve
 status approved

On Fri, 2009-04-24 at 15:40 +0000, Leonard Richardson wrote:
> Leonard Richardson has proposed merging
> lp:~leonardr/lazr.restful/representation_cache into lp:lazr.restful.

> === modified file 'src/lazr/restful/example/interfaces.py'
> --- src/lazr/restful/example/interfaces.py 2009-04-22 20:36:10 +0000
> +++ src/lazr/restful/example/interfaces.py 2009-04-24 15:14:07 +0000
> @@ -110,6 +110,8 @@
> description=u"The copyright date for this work."))
> description = exported(
> WhitespaceStrippingTextLine(title=u"Description", required=False))
> + revision_number = exported(Int(title=u"A record of the number of times "
> + "this cookbook has been modified."))

The line above should be indented to the same level of the beginning of
the argument's value on the previous line, like

    Int(foo='Bar and '
            'Baz')

> confirmed = exported(Bool(
> title=u"Whether this information has been confirmed",
> default=False))
>

>
--
Guilherme Salgado <email address hidden>

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/lazr/restful/_resource.py'
2--- src/lazr/restful/_resource.py 2009-04-15 00:24:52 +0000
3+++ src/lazr/restful/_resource.py 2009-04-24 15:28:26 +0000
4@@ -1309,11 +1309,6 @@
5 # Make the changes.
6 for field, (name, value) in validated_changeset.items():
7 field.set(self.entry, value)
8- # Clear any marshalled value for this field from the
9- # cache, so that the upcoming representation generation doesn't
10- # use the cached value.
11- if name in self._unmarshalled_field_cache:
12- del(self._unmarshalled_field_cache[name])
13 # The representation has changed, and etags will need to be
14 # recalculated.
15 self.etags_by_media_type = {}
16@@ -1325,6 +1320,20 @@
17 edited_fields=validated_changeset.keys())
18 notify(event)
19
20+ # The changeset contained new values for some of this object's
21+ # fields, and the notification event may have changed others.
22+ # Clear out any fields that changed.
23+ for name, ignored in self._unmarshalled_field_cache.items():
24+ force_clear = False
25+ try:
26+ old_value = getattr(entry_before_modification, name, None)
27+ new_value = getattr(self.entry.context, name, None)
28+ except Unauthorized:
29+ # Clear the cache, just to be safe.
30+ force_clear = True
31+ if force_clear or new_value != old_value:
32+ del(self._unmarshalled_field_cache[name])
33+
34 # If the modification caused the entry's URL to change, tell
35 # the client about the new URL.
36 new_url = absoluteURL(self.context, self.request)
37
38=== modified file 'src/lazr/restful/example/configure.zcml'
39--- src/lazr/restful/example/configure.zcml 2009-04-22 17:56:52 +0000
40+++ src/lazr/restful/example/configure.zcml 2009-04-24 15:14:07 +0000
41@@ -15,9 +15,9 @@
42 <class class="lazr.restful.example.root.Cookbook">
43 <require attributes="name copyright_date cover cuisine description
44 recipes find_recipe_for find_recipes last_printing
45- make_more_interesting price get"
46+ make_more_interesting price revision_number get"
47 set_attributes="name copyright_date cover cuisine description
48- last_printing price"
49+ last_printing price revision_number"
50 permission="zope.Public" />
51 <require attributes="confirmed"
52 set_attributes="confirmed"
53@@ -105,4 +105,10 @@
54 name="absolute_url"
55 />
56
57+ <subscriber
58+ for="lazr.restful.example.interfaces.ICookbook
59+ lazr.lifecycle.interfaces.IObjectModifiedEvent"
60+ handler="lazr.restful.example.subscribers.update_cookbook_revision_number" />
61+
62+
63 </configure>
64
65=== modified file 'src/lazr/restful/example/interfaces.py'
66--- src/lazr/restful/example/interfaces.py 2009-04-22 20:36:10 +0000
67+++ src/lazr/restful/example/interfaces.py 2009-04-24 15:14:07 +0000
68@@ -110,6 +110,8 @@
69 description=u"The copyright date for this work."))
70 description = exported(
71 WhitespaceStrippingTextLine(title=u"Description", required=False))
72+ revision_number = exported(Int(title=u"A record of the number of times "
73+ "this cookbook has been modified."))
74 confirmed = exported(Bool(
75 title=u"Whether this information has been confirmed",
76 default=False))
77
78=== modified file 'src/lazr/restful/example/root.py'
79--- src/lazr/restful/example/root.py 2009-04-22 19:52:53 +0000
80+++ src/lazr/restful/example/root.py 2009-04-24 15:14:07 +0000
81@@ -88,6 +88,7 @@
82 self.price = price
83 self.confirmed = confirmed
84 self.cover = None
85+ self.revision_number = 0
86
87 @property
88 def __name__(self):
89
90=== added file 'src/lazr/restful/example/subscribers.py'
91--- src/lazr/restful/example/subscribers.py 1970-01-01 00:00:00 +0000
92+++ src/lazr/restful/example/subscribers.py 2009-04-24 15:14:07 +0000
93@@ -0,0 +1,13 @@
94+# Copyright 2009 Canonical Ltd. All rights reserved.
95+
96+"""Event listeners for the example web service."""
97+
98+__metaclass__ = type
99+__all__ = ['update_cookbook_revision_number']
100+
101+from lazr.restful.example.interfaces import ICookbook
102+
103+def update_cookbook_revision_number(object, event):
104+ """Increment ICookbook.revision_number."""
105+ if ICookbook.providedBy(object):
106+ object.revision_number += 1
107
108=== modified file 'src/lazr/restful/example/tests/collection.txt'
109--- src/lazr/restful/example/tests/collection.txt 2009-04-22 19:52:53 +0000
110+++ src/lazr/restful/example/tests/collection.txt 2009-04-24 15:14:07 +0000
111@@ -136,6 +136,7 @@
112 price: 20
113 recipes_collection_link: u'http://.../cookbooks/The%20Joy%20of%20Cooking/recipes'
114 resource_type_link: u'http://...#cookbook'
115+ revision_number: 0
116 self_link: u'http://.../cookbooks/The%20Joy%20of%20Cooking'
117
118 A collection may be scoped to an element:
119
120=== modified file 'src/lazr/restful/example/tests/entry.txt'
121--- src/lazr/restful/example/tests/entry.txt 2009-04-16 20:45:55 +0000
122+++ src/lazr/restful/example/tests/entry.txt 2009-04-24 15:14:07 +0000
123@@ -266,17 +266,28 @@
124 Greens" cookbook. The data returned is the new JSON representation of
125 the object.
126
127- >>> print modify_cookbook('Everyday Greens', {'cuisine' : 'American'},
128- ... 'PATCH')
129+ >>> print webservice.get(greens_url).jsonBody()['revision_number']
130+ 0
131+
132+ >>> result = modify_cookbook('Everyday Greens', {'cuisine' : 'American'},
133+ ... 'PATCH')
134+ >>> print result
135 HTTP/1.1 209 Content Returned
136 ...
137 Content-Type: application/json
138 ...
139 {...}
140
141- >>> print webservice.get(greens_url).jsonBody()['cuisine']
142+ >>> greens = result.jsonBody()
143+ >>> print greens['cuisine']
144 American
145
146+Whenever a client modifies a cookbook, the revision_number is
147+incremented behind the scenes.
148+
149+ >>> print greens['revision_number']
150+ 1
151+
152 A modification might cause an entry's address to change. Here we use
153 the web service to change the cookbook's name to 'Everyday Greens 2'.
154
155@@ -363,7 +374,7 @@
156 When making a PATCH, you don't have to get a JSON representation
157 back. You can also get an HTML representation.
158
159- >>> print modify_cookbook('Everyday Greens', greens, 'PATCH',
160+ >>> print modify_cookbook('Everyday Greens', {}, 'PATCH',
161 ... headers={'Accept': 'application/xhtml+xml'})
162 HTTP/1.1 209 Content Returned
163 ...
164@@ -375,7 +386,7 @@
165 You can even get a WADL representation, though that's pretty useless.
166
167 >>> headers = {'Accept':'application/vd.sun.wadl+xml'}
168- >>> print modify_cookbook('Everyday Greens', greens, 'PATCH',
169+ >>> print modify_cookbook('Everyday Greens', {}, 'PATCH',
170 ... headers=headers)
171 HTTP/1.1 209 Content Returned
172 ...
173@@ -497,7 +508,8 @@
174 If you specify a number of ETags, and any of them match, your request
175 will go through.
176
177- >>> match = '"an-old-etag", %s' % greens_etag
178+ >>> greens = webservice.get(greens_url).jsonBody()
179+ >>> match = '"an-old-etag", %s' % greens['http_etag']
180 >>> print modify_cookbook('Everyday Greens', greens, 'PATCH',
181 ... {'If-Match' : match})
182 HTTP/1.1 209 Content Returned

Subscribers

People subscribed via source and target branches