Merge lp:~leonardr/launchpad/multi-part-etag into lp:launchpad/db-devel

Proposed by Leonard Richardson on 2010-03-17
Status: Merged
Approved by: Gary Poster on 2010-03-17
Approved revision: no longer in the source branch.
Merged at revision: not available
Proposed branch: lp:~leonardr/launchpad/multi-part-etag
Merge into: lp:launchpad/db-devel
Diff against target: 246 lines (+140/-26)
5 files modified
lib/canonical/launchpad/pagetests/webservice/apidoc.txt (+1/-0)
lib/canonical/launchpad/pagetests/webservice/conditional-write.txt (+103/-0)
utilities/apidoc-index.pt (+27/-8)
utilities/create-lp-wadl-and-apidoc.py (+8/-17)
versions.cfg (+1/-1)
To merge this branch: bzr merge lp:~leonardr/launchpad/multi-part-etag
Reviewer Review Type Date Requested Status
Gary Poster (community) 2010-03-17 Approve on 2010-03-17
Review via email: mp+21573@code.launchpad.net

Description of the Change

This branch does two things:

1. It upgrades lazr.restful to solve bug 336866. It also adds a Launchpad-specific test of the buggy behavior (even though the same behavior is tested in lazr.restful) so that Launchpad users can understand the behavior and so we'll have a starting point if the bug or similar bugs come back.

2. It makes some cosmetic changes to the generation of the apidoc. It generates the index file before generating the version-specific documentation (this is solely for my convenience, so I don't have to wait for all the docs to be generated when debugging the index generation), it simplifies the generation code slightly, and it styles the apidoc index.html with a small subset of the stylesheet used in the version-specific apidoc. This makes the apidoc index look consistent with the version-specific docs, instead of looking like a generic placeholder.

To post a comment you must log in.
Gary Poster (gary) wrote :

Great! The lazr.restful change seems like a great solution, and it is good to have the test in the launchpad tree.

The pagetemplate simplifications are small, especially in comparison to the importance of the fix for 336866, but they make me happy. :-)

Thanks

Gary

Gary Poster (gary) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/pagetests/webservice/apidoc.txt'
2--- lib/canonical/launchpad/pagetests/webservice/apidoc.txt 2010-03-15 16:14:46 +0000
3+++ lib/canonical/launchpad/pagetests/webservice/apidoc.txt 2010-03-17 20:31:01 +0000
4@@ -7,6 +7,7 @@
5 >>> browser.open('http://launchpad.dev/+apidoc')
6 >>> print extract_text(browser.contents)
7 Launchpad Web Service API
8+ ...
9 Launchpad web service API documentation
10 ...
11 Active versions
12
13=== added file 'lib/canonical/launchpad/pagetests/webservice/conditional-write.txt'
14--- lib/canonical/launchpad/pagetests/webservice/conditional-write.txt 1970-01-01 00:00:00 +0000
15+++ lib/canonical/launchpad/pagetests/webservice/conditional-write.txt 2010-03-17 20:31:01 +0000
16@@ -0,0 +1,103 @@
17+Conditional writes may work even when the ETags aren't identical
18+================================================================
19+
20+This code is tested in lazr.restful, but since it's proven a
21+longstanding problem in Launchpad, we're giving it a
22+Launchpad-specific test as well. This will give us a place to start if
23+the problem crops up again.
24+
25+Here's a bug: it has an ETag and values for fields like
26+'date_last_message'.
27+
28+ >>> url = '/bugs/1'
29+ >>> bug = webservice.get(url).jsonBody()
30+ >>> old_etag = bug['http_etag']
31+ >>> old_date_last_message = bug['date_last_message']
32+
33+When we add a message to a bug, 'date_last_message' is changed as a
34+side effect.
35+
36+ >>> print webservice.named_post(
37+ ... url, 'newMessage', subject="subject", content="content")
38+ HTTP/1.1 201 Created
39+ ...
40+
41+ >>> new_bug = webservice.get(url).jsonBody()
42+ >>> new_date_last_message = new_bug['date_last_message']
43+ >>> old_date_last_message == new_date_last_message
44+ False
45+
46+Because 'date_last_message' changed, the bug resource's ETag also
47+changed:
48+
49+ >>> new_etag = new_bug['http_etag']
50+ >>> old_etag == new_etag
51+ False
52+
53+A conditional GET request using the old ETag will fail, and the client
54+will hear about the new value for 'date_last_message'.
55+
56+ >>> print webservice.get(url, headers={'If-None-Match' : old_etag})
57+ HTTP/1.1 200 Ok
58+ ...
59+
60+But what if we want to PATCH the bug object after adding a message?
61+Logically speaking, the PATCH should go through. 'date_last_message' has
62+changed, but that's not a field that a client can modify
63+directly. There's no chance that my PATCH will modify
64+'date_last_message' in a way that conflicts with someone else's
65+PATCH. But in general, conditional write requests only succeed if the
66+client's value for If-Match exactly matches the server-side ETag.
67+
68+lazr.restful resolves this by splitting the ETag into two parts. The
69+first part changes only on changes to fields that clients cannot
70+modify directly, like 'date_last_message':
71+
72+ >>> old_read, old_write = old_etag.rsplit('-', 1)
73+ >>> new_read, new_write = new_etag.rsplit('-', 1)
74+ >>> old_read == new_read
75+ False
76+
77+The second part changes only on changes to fields that a client could
78+modify directly.
79+
80+ >>> old_write == new_write
81+ True
82+
83+So long as the second part of the submitted ETag matches, a
84+conditional write will succeed.
85+
86+ >>> import simplejson
87+ >>> data = simplejson.dumps({'title' : 'New title'})
88+ >>> headers = {'If-Match': old_etag}
89+ >>> print webservice.patch(url, 'application/json', data, headers=headers)
90+ HTTP/1.1 209 Content Returned
91+ ...
92+
93+Of course, now the resource has been modified by a client, and the
94+ETag has changed.
95+
96+ >>> newer_etag = webservice.get(url).jsonBody()['http_etag']
97+ >>> newer_read, newer_write = newer_etag.rsplit('-', 1)
98+
99+Both portions of the ETag has changed: the write portion because we
100+just changed 'description', and the read portion because
101+'date_last_updated' changed as a side effect.
102+
103+ >>> new_read == newer_read
104+ False
105+ >>> new_write == newer_write
106+ False
107+
108+A conditional write will fail when the write portion of the submitted
109+ETag doesn't match, even if the read portion matches.
110+
111+ >>> headers = {'If-Match': new_etag}
112+ >>> print webservice.patch(url, 'application/json', data, headers=headers)
113+ HTTP/1.1 412 Precondition Failed
114+ ...
115+
116+When two clients attempt overlapping modifications of the same
117+resource, the later one still gets a 412 error. If an unwritable field
118+changes, a conditional read will fail, but a conditional write will
119+succeed, even though the ETags don't match exactly.
120
121=== modified file 'utilities/apidoc-index.pt'
122--- utilities/apidoc-index.pt 2010-03-11 21:23:17 +0000
123+++ utilities/apidoc-index.pt 2010-03-17 20:31:01 +0000
124@@ -7,28 +7,47 @@
125 >
126 <head>
127 <title>Launchpad Web Service API</title>
128+
129+ <style type="text/css">
130+ body {
131+ font-family: sans-serif;
132+ font-size: 0.85em;
133+ margin: 2em 8em;
134+ }
135+ h1 {
136+ font-size: 2.5em;
137+ }
138+ h2 {
139+ border-bottom: 1px solid black;
140+ margin-top: 1em;
141+ margin-bottom: 0.5em;
142+ font-size: 2em;
143+ }
144+ ul {
145+ padding-left: 1.75em;
146+ }
147+ p + ul, p + ol, p + dl {
148+ margin-top: 0em;
149+ }
150+ </style>
151 </head>
152
153 <body>
154
155 <h1>Launchpad web service API documentation</h1>
156
157- <p tal:content="structure config/service_description">
158+ <p tal:content="structure options/config/service_description">
159 Description of the Launchpad web service.
160 </p>
161
162 <h2>Active versions</h2>
163
164 <ul>
165- <li tal:repeat="version_and_description versions_and_descriptions">
166-
167- <tal:version tal:define="version version_and_description/version;
168- description version_and_description/description">
169-
170+ <li tal:repeat="version options/config/active_versions">
171 <a tal:attributes="href string:$version.html"
172 tal:content="version">Version name</a>:
173- <tal:description replace="description" />
174- </tal:version>
175+ <tal:description
176+ replace="options/config/version_descriptions/?version" />
177 </li>
178 </ul>
179
180
181=== modified file 'utilities/create-lp-wadl-and-apidoc.py'
182--- utilities/create-lp-wadl-and-apidoc.py 2010-03-11 19:58:22 +0000
183+++ utilities/create-lp-wadl-and-apidoc.py 2010-03-17 20:31:01 +0000
184@@ -34,10 +34,18 @@
185 WebServiceApplication.cached_wadl = None # do not use cached file version
186 execute_zcml_for_scripts()
187 config = getUtility(IWebServiceConfiguration)
188+ directory, ignore = os.path.split(path_template)
189
190 stylesheet = pkg_resources.resource_filename(
191 'launchpadlib', 'wadl-to-refhtml.xsl')
192
193+ # First, create an index.html with links to all the HTML
194+ # documentation files we're about to generate.
195+ template_file = 'apidoc-index.pt'
196+ template = PageTemplateFile(template_file)
197+ f = open(os.path.join(directory, "index.html"), 'w')
198+ f.write(template(config=config))
199+
200 # Request the WADL from the root resource.
201 # We do this by creating a request object asking for a WADL
202 # representation.
203@@ -63,7 +71,6 @@
204
205 # Now, convert the WADL into an human-readable description and
206 # put the HTML in the same directory as the WADL.
207- directory, ignore = os.path.split(path_template)
208 html_filename = os.path.join(directory, version + ".html")
209 print "Writing apidoc for version %s to %s" % (
210 version, html_filename)
211@@ -71,22 +78,6 @@
212 subprocess.Popen(['xsltproc', stylesheet, filename], stdout=stdout)
213 stdout.close()
214
215- # Finally, create an index.html with links to all the HTML
216- # documentation files we just generated.
217- template_file = 'apidoc-index.pt'
218- template = PageTemplateFile(template_file)
219- namespace = template.pt_getContext()
220- namespace['config'] = config
221- versions_and_descriptions = []
222- for version in config.active_versions:
223- versions_and_descriptions.append(
224- dict(version=version,
225- description=config.version_descriptions[version]))
226- namespace['versions_and_descriptions'] = versions_and_descriptions
227-
228- f = open(os.path.join(directory, "index.html"), 'w')
229- f.write(template.pt_render(namespace))
230-
231 return 0
232
233 if __name__ == '__main__':
234
235=== modified file 'versions.cfg'
236--- versions.cfg 2010-03-11 20:53:58 +0000
237+++ versions.cfg 2010-03-17 20:31:01 +0000
238@@ -28,7 +28,7 @@
239 lazr.delegates = 1.1.0
240 lazr.enum = 1.1.2
241 lazr.lifecycle = 1.1
242-lazr.restful = 0.9.23
243+lazr.restful = 0.9.24
244 lazr.restfulclient = 0.9.12
245 lazr.smtptest = 1.1
246 lazr.testing = 0.1.1

Subscribers

People subscribed via source and target branches

to status/vote changes: