Merge lp:~leonardr/lazr.restful/cache-service-root into lp:lazr.restful

Proposed by Leonard Richardson
Status: Merged
Approved by: Gavin Panella
Approved revision: 126
Merged at revision: not available
Proposed branch: lp:~leonardr/lazr.restful/cache-service-root
Merge into: lp:lazr.restful
Diff against target: 217 lines (+145/-2)
5 files modified
src/lazr/restful/NEWS.txt (+12/-0)
src/lazr/restful/_resource.py (+30/-2)
src/lazr/restful/example/base/root.py (+1/-0)
src/lazr/restful/example/base/tests/root.txt (+88/-0)
src/lazr/restful/interfaces/_rest.py (+14/-0)
To merge this branch: bzr merge lp:~leonardr/lazr.restful/cache-service-root
Reviewer Review Type Date Requested Status
Gavin Panella Approve
Review via email: mp+22948@code.launchpad.net

Description of the change

This branch gets lazr.restful to set the Cache-Control header when serving a representation of the service root. This is a big win because the service root only changes when you deploy a new version of your lazr.restful application (eg. Launchpad), but every user makes 2 requests for it every time they start up lazr.restfulclient.

There are two 'max-age' times set in the Cache-Control header: one for the latest version of the web service (which changes more often) and one for all other versions. By default, the max-age for the latest version is one hour and the max-age for all other versions is one week.

After the max-age expires, the client will make a *conditional* request to see whether the service root has changed. If it hasn't, I believe the client will update the client-side max-age and not make any requests for another hour (or week). But I need to check to make sure httplib2 actually does this.

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote :

Nice branch. Conversation from IRC:

<allenap> leonardr: Is there any way config.active_versions could be empty?
<leonardr> allenap: no, that would prevent lazr.restful from starting up
<allenap> leonardr: Instead of having [604800, 3600], could you have a module-level constant like `HOUR = 3600 # seconds`, then the default can be specified as [7 * 24 * HOUR, 1 * HOUR]?
<leonardr> sure

review: Approve
127. By Leonard Richardson

Set the Date header as well, since httplib2 uses it to determine whether a cached representation is stale.

Revision history for this message
Leonard Richardson (leonardr) wrote :

In my experiments with lazr.restfulclient I discovered that httplib2 will ignore Cache-Control unless the Date header is also set to provide a starting point. I've updated the branch to set the Date header.

Revision history for this message
Gavin Panella (allenap) wrote :

The late addition, r127, looks good too.

review: Approve
128. By Leonard Richardson

Set Cache-Control and Date even on a conditional request.

129. By Leonard Richardson

Set caching headers whether the request was conditional or not.

Revision history for this message
Leonard Richardson (leonardr) wrote :

UNfortunately, this branch breaks lazr.restfulclient due to what I believe is a bug in httplib2 (http://code.google.com/p/httplib2/issues/detail?id=97). We can still land this branch, but we'll only be able to serve the goodness to clients that signal they can work around the bug. This means changing clients to send custom values for User-Agent.

130. By Leonard Richardson

Don't serve cache control headers to httplib2 clients to avoid triggering a bug in httplib2.

131. By Leonard Richardson

Removed temporary hack for testing purposes.

Revision history for this message
Leonard Richardson (leonardr) wrote :

All right, I've changed lazr.restful to avoid triggering the httplib2 bug on old versions of lazr.restfulclient. (I've tested this version of lazr.restful with an old and a new lazr.restfulclient, and they both worked, though obviously the old one didn't get the benefit of the Cache-Control headers.)

132. By Leonard Richardson

Slight refactoring.

Revision history for this message
Gavin Panella (allenap) wrote :

Still looking good :)

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/NEWS.txt'
2--- src/lazr/restful/NEWS.txt 2010-03-17 14:38:16 +0000
3+++ src/lazr/restful/NEWS.txt 2010-04-13 15:37:34 +0000
4@@ -2,6 +2,18 @@
5 NEWS for lazr.restful
6 =====================
7
8+Development
9+===========
10+
11+Special note: This version introduces a new configuration element,
12+'caching_policy'. This element starts out simple but may become more
13+complex in future versions. See the IWebServiceConfiguration interface
14+for more details.
15+
16+Service root resources are now client-side cacheable for an amount of
17+time that depends on the server configuration and the version of the
18+web service requested.
19+
20 0.9.24 (2010-03-17)
21 ====================
22
23
24=== modified file 'src/lazr/restful/_resource.py'
25--- src/lazr/restful/_resource.py 2010-03-16 15:07:00 +0000
26+++ src/lazr/restful/_resource.py 2010-04-13 15:37:34 +0000
27@@ -32,9 +32,11 @@
28 import copy
29 from cStringIO import StringIO
30 from datetime import datetime, date
31+from email.Utils import formatdate
32 from gzip import GzipFile
33 import os
34 import simplejson
35+import time
36 import zlib
37
38 # Import SHA in a way compatible with both Python 2.4 and Python 2.6.
39@@ -1621,10 +1623,36 @@
40 result = ""
41 return self.applyTransferEncoding(result)
42
43+ def setCachingHeaders(self):
44+ "How long should the client cache this service root?"
45+ user_agent = self.request.getHeader('User-Agent', '')
46+ if user_agent.startswith('Python-httplib2'):
47+ # XXX leonardr 20100412
48+ # bug=http://code.google.com/p/httplib2/issues/detail?id=97
49+ #
50+ # A client with a User-Agent of "Python/httplib2" (such as
51+ # old versions of lazr.restfulclient) gives inconsistent
52+ # results when a resource is served with both ETag and
53+ # Cache-Control. We check for that User-Agent and omit the
54+ # Cache-Control headers if it makes a request.
55+ return
56+ config = getUtility(IWebServiceConfiguration)
57+ caching_policy = config.caching_policy
58+ if self.request.version == config.active_versions[-1]:
59+ max_age = caching_policy[-1]
60+ else:
61+ max_age = caching_policy[0]
62+ if max_age > 0:
63+ self.request.response.setHeader(
64+ 'Cache-Control', 'max-age=%d' % max_age)
65+ # Also set the Date header so that client-side caches will
66+ # have something to work from.
67+ self.request.response.setHeader('Date', formatdate(time.time()))
68+
69 def do_GET(self):
70- """Describe the capabilities of the web service in WADL."""
71-
72+ """Describe the capabilities of the web service."""
73 media_type = self.handleConditionalGET()
74+ self.setCachingHeaders()
75 if media_type is None:
76 # The conditional GET succeeded. Serve nothing.
77 return ""
78
79=== modified file 'src/lazr/restful/example/base/root.py'
80--- src/lazr/restful/example/base/root.py 2010-03-10 18:45:04 +0000
81+++ src/lazr/restful/example/base/root.py 2010-04-13 15:37:34 +0000
82@@ -385,6 +385,7 @@
83
84 class WebServiceConfiguration(BaseWebServiceConfiguration):
85 directives.publication_class(WebServiceTestPublication)
86+ caching_policy = [10000, 2]
87 code_revision = 'test.revision'
88 default_batch_size = 5
89 hostname = 'cookbooks.dev'
90
91=== modified file 'src/lazr/restful/example/base/tests/root.txt'
92--- src/lazr/restful/example/base/tests/root.txt 2010-02-11 17:57:16 +0000
93+++ src/lazr/restful/example/base/tests/root.txt 2010-04-13 15:37:34 +0000
94@@ -173,3 +173,91 @@
95
96 >>> print top_level_links['featured_cookbook_link']
97 http://.../cookbooks/featured
98+
99+Caching policy
100+==============
101+
102+The service root resource is served with the Cache-Control header
103+giving a configurable value for "max-age". An old version of the
104+service root can be cached for a long time:
105+
106+ >>> response = webservice.get('/', api_version='1.0')
107+ >>> print response.getheader('Cache-Control')
108+ max-age=10000
109+
110+The latest version of the service root should be cached for less time.
111+
112+ >>> response = webservice.get('/', api_version='devel')
113+ >>> print response.getheader('Cache-Control')
114+ max-age=2
115+
116+Both the WADL and JSON representations of the service root are
117+cacheable.
118+
119+ >>> wadl_type = 'application/vnd.sun.wadl+xml'
120+ >>> response = webservice.get('/', wadl_type)
121+ >>> print response.getheader('Cache-Control')
122+ max-age=2
123+
124+The Date header is set along with Cache-Control so that the client can
125+easily determine when the cache is stale.
126+
127+ >>> response.getheader('Date') is None
128+ False
129+
130+Date and Cache-Control are set even when the request is a conditional
131+request where the condition failed.
132+
133+ >>> etag = response.getheader('ETag')
134+ >>> conditional_response = webservice.get(
135+ ... '/', wadl_type, headers={'If-None-Match' : etag})
136+ >>> conditional_response.status
137+ 304
138+ >>> print conditional_response.getheader('Cache-Control')
139+ max-age=2
140+ >>> conditional_response.getheader('Date') is None
141+ False
142+
143+To avoid triggering a bug in httplib2, lazr.restful does not send the
144+Cache-Control or Date headers to clients that identify as
145+Python-httplib2.
146+
147+ # XXX leonardr 20100412
148+ # bug=http://code.google.com/p/httplib2/issues/detail?id=97
149+ >>> agent = 'Python-httplib2/$Rev: 259$'
150+ >>> response = webservice.get(
151+ ... '/', wadl_type, headers={'User-Agent' : agent})
152+ >>> print response.getheader('Cache-Control')
153+ None
154+ >>> print response.getheader('Date')
155+ None
156+
157+If the client identifies as an agent _based on_ httplib2, we take a
158+chance and send the Cache-Control headers.
159+
160+ >>> agent = "Custom client (%s)" % agent
161+ >>> response = webservice.get(
162+ ... '/', wadl_type, headers={'User-Agent' : agent})
163+ >>> print response.getheader('Cache-Control')
164+ max-age=2
165+ >>> response.getheader('Date') is None
166+ False
167+
168+If the caching policy says not to cache the service root resource at
169+all, the Cache-Control and Date headers are not present.
170+
171+ >>> from zope.component import getUtility
172+ >>> from lazr.restful.interfaces import IWebServiceConfiguration
173+ >>> policy = getUtility(IWebServiceConfiguration).caching_policy
174+ >>> old_value = policy[-1]
175+ >>> policy[-1] = 0
176+
177+ >>> response = webservice.get('/')
178+ >>> print response.getheader('Cache-Control')
179+ None
180+ >>> response.getheader('Date') is None
181+ True
182+
183+Cleanup.
184+
185+ >>> policy[-1] = old_value
186
187=== modified file 'src/lazr/restful/interfaces/_rest.py'
188--- src/lazr/restful/interfaces/_rest.py 2010-03-10 19:03:11 +0000
189+++ src/lazr/restful/interfaces/_rest.py 2010-04-13 15:37:34 +0000
190@@ -63,6 +63,9 @@
191 IBrowserRequest, IDefaultBrowserLayer)
192 from lazr.batchnavigator.interfaces import InvalidBatchSizeError
193
194+# Constants for periods of time
195+HOUR = 3600 # seconds
196+
197 # The namespace prefix for LAZR web service-related tags.
198 LAZR_WEBSERVICE_NS = 'lazr.restful'
199
200@@ -394,6 +397,17 @@
201 These are miscellaneous strings that may differ in different web
202 services.
203 """
204+ caching_policy = List(
205+ value_type=Int(),
206+ default = [7 * 24 * HOUR, 1 * HOUR],
207+ title=u"The web service caching policy.",
208+ description = u"""A list of two numbers, each to be used in the
209+ 'max-age' field of the Cache-Control header. The first number is
210+ used when serving the service root for any web service version
211+ except the latest one. The second number is used when serving the
212+ service root for the latest version (which probably changes more
213+ often).""")
214+
215 service_description = TextLine(
216 title=u"Service description",
217 description=u"""A human-readable description of the web service.

Subscribers

People subscribed via source and target branches