Merge lp:~leonardr/launchpad/test-representation-cache into lp:launchpad

Proposed by Leonard Richardson
Status: Merged
Merged at revision: 11011
Proposed branch: lp:~leonardr/launchpad/test-representation-cache
Merge into: lp:launchpad
Diff against target: 323 lines (+220/-10)
8 files modified
lib/canonical/database/sqlbase.py (+7/-0)
lib/canonical/launchpad/pagetests/webservice/cache.txt (+55/-0)
lib/canonical/launchpad/zcml/webservice.zcml (+5/-0)
lib/lp/services/memcache/client.py (+3/-1)
lib/lp/services/memcache/doc/restful-cache.txt (+102/-0)
lib/lp/services/memcache/restful.py (+39/-0)
lib/lp/services/memcache/tests/test_doc.py (+8/-8)
versions.cfg (+1/-1)
To merge this branch: bzr merge lp:~leonardr/launchpad/test-representation-cache
Reviewer Review Type Date Requested Status
Stuart Bishop (community) Approve
Brad Crittenden (community) code Approve
Review via email: mp+26513@code.launchpad.net

Description of the change

This branch integrates version 0.9.27 of lazr.restful to create a memcached-based cache for JSON representations of entries. In tests with a populated cache, (https://dev.launchpad.net/Foundations/Webservice/Performance#Store%20representations%20in%20memcached) this reduced the time to receive a representation of a collection by about 4x.

lazr.restful takes care of putting representations in the cache and retrieving them at appropriate times. This code takes care of invalidating the cache when a Storm object changes, and turning a Storm object into a unique cache key.

I plan to make one more change to this branch: make it easy to for the LOSAs to turn the cache off if it turns out to cause problems in production. I'm not really sure how to do this, so I'm presenting the branch as is as I work on this other problem.

To post a comment you must log in.
Revision history for this message
Brad Crittenden (bac) wrote :

Hi Leonard,

Thanks for this branch -- it looks very promising. A 4x increase would be great.

It looks like you haven't pushed your changes to the download cache up yet so lazr.restful 0.9.27 isn't available. Be sure to do that.

You define but don't use 'json_type' in webservice/cache.txt.

The __all__ in memcache/client.py should be on multiple lines.

It looks like there are some valid complaints from 'make lint'. Please run it and clean up the ones that are real.

review: Approve (code)
Revision history for this message
Stuart Bishop (stub) wrote :
Download full text (4.6 KiB)

On Tue, Jun 1, 2010 at 11:12 PM, Leonard Richardson
<email address hidden> wrote:

> === modified file 'lib/canonical/database/sqlbase.py'

> +    def __storm_flushed__(self):
> +        """Invalidate the web service cache."""
> +        cache = getUtility(IRepresentationCache)
> +        cache.delete(self)

This looks like a decent way of hooking in. Unfortunatly, SQLBase is
legacy - used by objects providing the SQLObject compatibility layer.
Some of our newer classes use the Storm base class directly. If there
is no way to register an on-flush hook with Storm, you will need to
create a Launchpad specific subclass of storm.Storm that provides this
hook, and update classes using the Storm base class directly to use
LPStorm.

The cache can become stale by:

1) Raw SQL, at psql command line or executed by appservers or scripts.
2) Stored procedures
3) Store.remove()
4) Using Storm for bulk updates, inserts or deletions.
5) Scripts or appservers running with non-identical [memcache] config section.
6) Scripts or appservers unable to contact memcached servers
7) Network glitches

We should probably ignore the last three. Are the first four a problem
though for the webservice? If it doesn't really matter, fine.
Otherwise we might need some ON DELETE and ON UPDATE database triggers
to keep things in sync. Hopefully this is unnecessary, as it could
will kill our performance gain.

=== added file 'lib/canonical/launchpad/pagetests/webservice/cache.txt'

+The cache starts out empty.
+
+ >>> print cache.get_by_key(key)
+ None
+
+Retrieving a representation of an object populates the cache.
+
+ >>> ignore = webservice.get("/~salgado", api_version="devel").jsonBody()
+
+ >>> cache.get_by_key(key)
+ '{...}'

Is webservice.get making a real connection to the webservice here? I
am curious if the cache is populated when the object is retrieved,
rather than when the transaction that retrieved the object commits.

=== added file 'lib/lp/services/memcache/doc/restful-cache.txt'

> +An object's cache key is derived from its Storm metadata: its database
> +table name and its primary key.

> +    >>> cache_key = cache.key_for(
> +    ...     person, 'media/type', 'web-service-version')
> +    >>> print person.id, cache_key
> +    29 Person(29,),media/type,web-service-version

Do we need the LPCONFIG here too? Or is it ok for edge & production to mix?

Hmm... perhaps IMemcacheClient should grow a real API and
automatically prepend the LPCONFIG - not sure if have a use case for
edge and production sharing data, but it is convenient to have them
share the same physical Memcached servers.

> +When a Launchpad object is modified, its JSON representations for
> +recognized web service versions are automatically removed from the
> +cache.
> +
> +    >>> person.addressline1 = "New address"
> +    >>> from canonical.launchpad.ftests import syncUpdate
> +    >>> syncUpdate(person)
> +
> +    >>> print cache.get(person, json_type, "beta", default="missing")
> +    missing
> +
> +    >>> print cache.get(person, json_type, "1.0", default="missing")
> +    missing

Should we document the cases where this doesn't happen, or is this
irrelevant to the webservice?
...

Read more...

Revision history for this message
Stuart Bishop (stub) :
review: Approve
Revision history for this message
Leonard Richardson (leonardr) wrote :
Download full text (4.5 KiB)

> 1) Raw SQL, at psql command line or executed by appservers or scripts.
> 2) Stored procedures
> 3) Store.remove()
> 4) Using Storm for bulk updates, inserts or deletions.
> 5) Scripts or appservers running with non-identical [memcache] config section.
> 6) Scripts or appservers unable to contact memcached servers
> 7) Network glitches
>
> We should probably ignore the last three. Are the first four a problem
> though for the webservice? If it doesn't really matter, fine.
> Otherwise we might need some ON DELETE and ON UPDATE database triggers
> to keep things in sync. Hopefully this is unnecessary, as it could
> will kill our performance gain.

Certainly the first four are a problem. So far we have yet to come to
terms with performance improvements that involve serving stale data from
the web service (or allowing users to use the possibly stale data they
already have).

Can we put the invalidation code in the ON DELETE/ON UPDATE trigger,
rather than in the Storm trigger? Would it hurt performance to just move
the invalidation down a layer?

> + >>> ignore = webservice.get("/~salgado", api_version="devel").jsonBody()
> +
> + >>> cache.get_by_key(key)
> + '{...}'
>
> Is webservice.get making a real connection to the webservice here? I
> am curious if the cache is populated when the object is retrieved,
> rather than when the transaction that retrieved the object commits.

An HTTP request is being constructed and through Python machinations it
is invoking the web service code. But nothing's going over the network.

The cache is populated by the web service code during the GET handling.
Cache population shouldn't have anything to do with the database.

I hope this answers your question.

> > + >>> cache_key = cache.key_for(
> > + ... person, 'media/type', 'web-service-version')
> > + >>> print person.id, cache_key
> > + 29 Person(29,),media/type,web-service-version
>
> Do we need the LPCONFIG here too? Or is it ok for edge & production to mix?
>
> Hmm... perhaps IMemcacheClient should grow a real API and
> automatically prepend the LPCONFIG - not sure if have a use case for
> edge and production sharing data, but it is convenient to have them
> share the same physical Memcached servers.

Aaaah, it's absolutely not OK for edge and production to mix data. The
JSON representations include http: links to one server or the other. I
can fix this in the branch by tacking the instance name on to the cache
key.

> Should we document the cases where this doesn't happen, or is this
> irrelevant to the webservice?
>
> store.execute(Update({Person.addressline1: 'Newer Address'},
> Person.name==person.name)) should make the cache stale.

I'd say that's relevant, and another argument for putting the cache
invalidation in a database hook rather than an ORM hook.

>
> > === added file 'lib/lp/services/memcache/restful.py'
>
> > +class MemcachedStormRepresentationCache(BaseRepresentationCache):
> > + """Caches lazr.restful representations of Storm objects in memcached."""
> > +
> > + def __init__(self):
> > + self.client = memcache_client_factory()
>
> You should be using getUtility(IMemcacheClient) here - otherwise we
> end...

Read more...

Revision history for this message
Stuart Bishop (stub) wrote :
Download full text (5.2 KiB)

On Wed, Jun 2, 2010 at 7:28 PM, Leonard Richardson
<email address hidden> wrote:
>> 1) Raw SQL, at psql command line or executed by appservers or scripts.
>> 2) Stored procedures
>> 3) Store.remove()
>> 4) Using Storm for bulk updates, inserts or deletions.
>> 5) Scripts or appservers running with non-identical [memcache] config section.
>> 6) Scripts or appservers unable to contact memcached servers
>> 7) Network glitches
>>
>> We should probably ignore the last three. Are the first four a problem
>> though for the webservice? If it doesn't really matter, fine.
>> Otherwise we might need some ON DELETE and ON UPDATE database triggers
>> to keep things in sync. Hopefully this is unnecessary, as it could
>> will kill our performance gain.
>
> Certainly the first four are a problem. So far we have yet to come to
> terms with performance improvements that involve serving stale data from
> the web service (or allowing users to use the possibly stale data they
> already have).
>
> Can we put the invalidation code in the ON DELETE/ON UPDATE trigger,
> rather than in the Storm trigger? Would it hurt performance to just move
> the invalidation down a layer?

We can put the invalidation code in a trigger.

It will certainly hurt performance. How much, I'm not sure. It also
depends on how much effort we put into minimizing it. The stored
procedure will need to be invoked for every row updated or deleted.

The simplest approach would be a Python stored procedure for the
trigger that invalidates the cache in band. This would be huge
overhead, both the Python overhead and waiting for network round
trips.

A better approach would be for the trigger to notify another process
of the keys that need invalidating, and have that process do it
asynchronously. This would be less overhead than our existing Slony-I
replication triggers. There would be some lag.

An alternative, which I'd need to investigate, would be to piggy back
on the existing Slony-I replication information. Whenever a row is
updated or deleted, we generate events containing this information so
the changes can be applied on the subscriber database nodes. There
would be some lag here too, but no measurable database impact.

>> + >>> ignore = webservice.get("/~salgado", api_version="devel").jsonBody()
>> +
>> + >>> cache.get_by_key(key)
>> + '{...}'
>>
>> Is webservice.get making a real connection to the webservice here? I
>> am curious if the cache is populated when the object is retrieved,
>> rather than when the transaction that retrieved the object commits.
>
> An HTTP request is being constructed and through Python machinations it
> is invoking the web service code. But nothing's going over the network.
>
> The cache is populated by the web service code during the GET handling.
> Cache population shouldn't have anything to do with the database.
>
> I hope this answers your question.

I'm wondering if it is possible that the cache gets populated, but the
webservice request fails, causing an invalid value to get stored. If
we don't populate the cache on updates then it should be fine.

>> > === added file 'lib/lp/services/memcache/restful.py'
>>
>> > +class MemcachedStormRepresen...

Read more...

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

> A better approach would be for the trigger to notify another process
> of the keys that need invalidating, and have that process do it
> asynchronously. This would be less overhead than our existing Slony-I
> replication triggers. There would be some lag.
>
> An alternative, which I'd need to investigate, would be to piggy back
> on the existing Slony-I replication information. Whenever a row is
> updated or deleted, we generate events containing this information so
> the changes can be applied on the subscriber database nodes. There
> would be some lag here too, but no measurable database impact.

I'm +1 on either of these ideas. The only downside is that the invalidation algorithm will become more complex over time, as I make lazr.restful cache more things and as we create more versions of the Launchpad web service.

Here's what I mean. Right now when User(29,) is invalidated we need to remove the following three cache keys:

User(29,),application/json,beta
User(29,),application/json,1.0
User(29,),application/json,devel

That's one key for every web service version. We need to have access to a list of the currently active versions, either managed separately or obtained from getUtility(IWebServiceConfiguration).active_Versions.

You also brought up the fact that all our Launchpad instances share memcached hardware. So we need to change Launchpad's key generation to include the instance name. And we need to change the invalidation code to have a list of all instance names, and to invalidate these nine keys:

User(29,),application/json,beta,edge
User(29,),application/json,beta,staging
User(29,),application/json,beta,production
User(29,),application/json,1.0,edge
User(29,),application/json,1.0,staging
User(29,),application/json,1.0,production
User(29,),application/json,devel,edge
User(29,),application/json,devel,staging
User(29,),application/json,devel,production

In the future we may change lazr.restful to cache the WADL and/or HTML representations as well as the JSON representation. In that case there could be up to 27 cache keys for User(29,).

This isn't unmanageable, but it would be much simpler if we could tell memcached to delete "User(29,).*"

> I'm wondering if it is possible that the cache gets populated, but the
> webservice request fails, causing an invalid value to get stored. If
> we don't populate the cache on updates then it should be fine.

I'm pretty sure the cache is populated right before the representation is returned to the client. If there's an error it will happen during the representation creation. If for some reason an error happens afterwards it doesn't change the fact that the representation is accurate. The cache is certainly not populated on updates. So I think we're OK.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/canonical/database/sqlbase.py'
--- lib/canonical/database/sqlbase.py 2010-03-24 17:37:26 +0000
+++ lib/canonical/database/sqlbase.py 2010-06-01 20:53:29 +0000
@@ -28,6 +28,8 @@
28from zope.interface import implements28from zope.interface import implements
29from zope.security.proxy import removeSecurityProxy29from zope.security.proxy import removeSecurityProxy
3030
31from lazr.restful.interfaces import IRepresentationCache
32
31from canonical.config import config, dbconfig33from canonical.config import config, dbconfig
32from canonical.database.interfaces import ISQLBase34from canonical.database.interfaces import ISQLBase
3335
@@ -246,6 +248,11 @@
246 """Inverse of __eq__."""248 """Inverse of __eq__."""
247 return not (self == other)249 return not (self == other)
248250
251 def __storm_flushed__(self):
252 """Invalidate the web service cache."""
253 cache = getUtility(IRepresentationCache)
254 cache.delete(self)
255
249alreadyInstalledMsg = ("A ZopelessTransactionManager with these settings is "256alreadyInstalledMsg = ("A ZopelessTransactionManager with these settings is "
250"already installed. This is probably caused by calling initZopeless twice.")257"already installed. This is probably caused by calling initZopeless twice.")
251258
252259
=== added file 'lib/canonical/launchpad/pagetests/webservice/cache.txt'
--- lib/canonical/launchpad/pagetests/webservice/cache.txt 1970-01-01 00:00:00 +0000
+++ lib/canonical/launchpad/pagetests/webservice/cache.txt 2010-06-01 20:53:29 +0000
@@ -0,0 +1,55 @@
1************************
2The representation cache
3************************
4
5Launchpad stores JSON representations of objects in a memcached
6cache. The full cache functionality is tested in lazr.restful and in
7lib/lp/services/memcache/doc/restful-cache.txt. This is just a simple
8integration test.
9
10First we need to get a reference to the cache object, so we can look
11inside.
12
13 >>> from zope.component import getUtility
14 >>> from lazr.restful.interfaces import IRepresentationCache
15 >>> cache = getUtility(IRepresentationCache)
16
17Since the cache is keyed by the underlying database object, we also
18need one of those objects.
19
20 >>> from lp.registry.interfaces.person import IPersonSet
21 >>> login(ANONYMOUS)
22 >>> person = getUtility(IPersonSet).getByName('salgado')
23 >>> key = cache.key_for(person, 'application/json', 'devel')
24 >>> logout()
25
26The cache starts out empty.
27
28 >>> print cache.get_by_key(key)
29 None
30
31Retrieving a representation of an object populates the cache.
32
33 >>> ignore = webservice.get("/~salgado", api_version="devel").jsonBody()
34
35 >>> cache.get_by_key(key)
36 '{...}'
37
38Once the cache is populated with a representation, the cached
39representation is used in preference to generating a new
40representation of that object. We can verify this by putting a fake
41value into the cache and retrieving a representation of the
42corresponding object.
43
44 >>> import simplejson
45 >>> cache.set_by_key(key, simplejson.dumps("Fake representation"))
46
47 >>> print webservice.get("/~salgado", api_version="devel").jsonBody()
48 Fake representation
49
50Cleanup.
51
52 >>> cache.delete(person)
53
54 >>> webservice.get("/~salgado", api_version="devel").jsonBody()
55 {...}
056
=== modified file 'lib/canonical/launchpad/zcml/webservice.zcml'
--- lib/canonical/launchpad/zcml/webservice.zcml 2010-03-26 19:11:50 +0000
+++ lib/canonical/launchpad/zcml/webservice.zcml 2010-06-01 20:53:29 +0000
@@ -16,6 +16,11 @@
16 provides="lazr.restful.interfaces.IWebServiceConfiguration">16 provides="lazr.restful.interfaces.IWebServiceConfiguration">
17 </utility>17 </utility>
1818
19 <utility
20 factory="lp.services.memcache.restful.MemcachedStormRepresentationCache"
21 provides="lazr.restful.interfaces.IRepresentationCache">
22 </utility>
23
19 <securedutility24 <securedutility
20 class="canonical.launchpad.systemhomes.WebServiceApplication"25 class="canonical.launchpad.systemhomes.WebServiceApplication"
21 provides="canonical.launchpad.interfaces.IWebServiceApplication">26 provides="canonical.launchpad.interfaces.IWebServiceApplication">
2227
=== modified file 'lib/lp/services/memcache/client.py'
--- lib/lp/services/memcache/client.py 2009-09-16 12:47:23 +0000
+++ lib/lp/services/memcache/client.py 2010-06-01 20:53:29 +0000
@@ -4,7 +4,9 @@
4"""Launchpad Memcache client."""4"""Launchpad Memcache client."""
55
6__metaclass__ = type6__metaclass__ = type
7__all__ = []7__all__ = [
8 'memcache_client_factory'
9 ]
810
9import memcache11import memcache
10import re12import re
1113
=== added file 'lib/lp/services/memcache/doc/restful-cache.txt'
--- lib/lp/services/memcache/doc/restful-cache.txt 1970-01-01 00:00:00 +0000
+++ lib/lp/services/memcache/doc/restful-cache.txt 2010-06-01 20:53:29 +0000
@@ -0,0 +1,102 @@
1***************************************
2The Storm/memcached representation cache
3****************************************
4
5The web service library lazr.restful will store the representations it
6generates in a cache, if a suitable cache implementation is
7provided. We implement a cache that stores representations of Storm
8objects in memcached.
9
10 >>> login('foo.bar@canonical.com')
11
12 >>> from lp.services.memcache.restful import (
13 ... MemcachedStormRepresentationCache)
14 >>> cache = MemcachedStormRepresentationCache()
15
16An object's cache key is derived from its Storm metadata: its database
17table name and its primary key.
18
19 >>> from zope.component import getUtility
20 >>> from lp.registry.interfaces.person import IPersonSet
21 >>> person = getUtility(IPersonSet).getByName('salgado')
22
23 >>> cache_key = cache.key_for(
24 ... person, 'media/type', 'web-service-version')
25 >>> print person.id, cache_key
26 29 Person(29,),media/type,web-service-version
27
28 >>> from operator import attrgetter
29 >>> languages = sorted(person.languages, key=attrgetter('englishname'))
30 >>> for language in languages:
31 ... cache_key = cache.key_for(
32 ... language, 'media/type', 'web-service-version')
33 ... print language.id, cache_key
34 119 Language(119,),media/type,web-service-version
35 521 Language(521,),media/type,web-service-version
36
37The cache starts out empty.
38
39 >>> json_type = 'application/json'
40
41 >>> print cache.get(person, json_type, "v1", default="missing")
42 missing
43
44Add a representation to the cache, and you can retrieve it later.
45
46 >>> cache.set(person, json_type, "beta",
47 ... "This is a representation for version beta.")
48
49 >>> print cache.get(person, json_type, "beta")
50 This is a representation for version beta.
51
52A single object can cache different representations for different
53web service versions.
54
55 >>> cache.set(person, json_type, '1.0',
56 ... 'This is a different representation for version 1.0.')
57
58 >>> print cache.get(person, json_type, "1.0")
59 This is a different representation for version 1.0.
60
61The web service version doesn't have to actually be defined in the
62configuration. (But you shouldn't use this--see below!)
63
64 >>> cache.set(person, json_type, 'no-such-version',
65 ... 'This is a representation for a nonexistent version.')
66
67 >>> print cache.get(person, json_type, "no-such-version")
68 This is a representation for a nonexistent version.
69
70A single object can also cache different representations for different
71media types, not just application/json. (But you shouldn't use
72this--see below!)
73
74 >>> cache.set(person, 'media/type', '1.0',
75 ... 'This is a representation for a strange media type.')
76
77 >>> print cache.get(person, "media/type", "1.0")
78 This is a representation for a strange media type.
79
80When a Launchpad object is modified, its JSON representations for
81recognized web service versions are automatically removed from the
82cache.
83
84 >>> person.addressline1 = "New address"
85 >>> from canonical.launchpad.ftests import syncUpdate
86 >>> syncUpdate(person)
87
88 >>> print cache.get(person, json_type, "beta", default="missing")
89 missing
90
91 >>> print cache.get(person, json_type, "1.0", default="missing")
92 missing
93
94But non-JSON representations, and representations for unrecognized web
95service versions, are _not_ removed from the cache. (This is why you
96shouldn't put such representations into the cache.)
97
98 >>> print cache.get(person, json_type, "no-such-version")
99 This is a representation for a nonexistent version.
100
101 >>> print cache.get(person, "media/type", "1.0")
102 This is a representation for a strange media type.
0103
=== added file 'lib/lp/services/memcache/restful.py'
--- lib/lp/services/memcache/restful.py 1970-01-01 00:00:00 +0000
+++ lib/lp/services/memcache/restful.py 2010-06-01 20:53:29 +0000
@@ -0,0 +1,39 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Storm/memcached implementation of lazr.restful's representation cache."""
5
6import storm
7
8from lp.services.memcache.client import memcache_client_factory
9from lazr.restful.simple import BaseRepresentationCache
10
11__metaclass__ = type
12__all__ = [
13 'MemcachedStormRepresentationCache',
14]
15
16
17class MemcachedStormRepresentationCache(BaseRepresentationCache):
18 """Caches lazr.restful representations of Storm objects in memcached."""
19
20 def __init__(self):
21 self.client = memcache_client_factory()
22
23 def key_for(self, obj, media_type, version):
24 storm_info = storm.info.get_obj_info(obj)
25 table_name = storm_info.cls_info.table
26 primary_key = tuple(var.get() for var in storm_info.primary_vars)
27
28 key = (table_name + repr(primary_key)
29 + ',' + media_type + ',' + str(version))
30 return key
31
32 def get_by_key(self, key, default=None):
33 return self.client.get(key) or default
34
35 def set_by_key(self, key, value):
36 self.client.set(key, value)
37
38 def delete_by_key(self, key):
39 self.client.delete(key)
040
=== modified file 'lib/lp/services/memcache/tests/test_doc.py'
--- lib/lp/services/memcache/tests/test_doc.py 2010-03-03 11:00:42 +0000
+++ lib/lp/services/memcache/tests/test_doc.py 2010-06-01 20:53:29 +0000
@@ -7,9 +7,7 @@
77
8import os.path8import os.path
9from textwrap import dedent9from textwrap import dedent
10import unittest
1110
12from zope.component import getUtility
13import zope.pagetemplate.engine11import zope.pagetemplate.engine
14from zope.pagetemplate.pagetemplate import PageTemplate12from zope.pagetemplate.pagetemplate import PageTemplate
15from zope.publisher.browser import TestRequest13from zope.publisher.browser import TestRequest
@@ -17,9 +15,7 @@
17from canonical.launchpad.testing.systemdocs import (15from canonical.launchpad.testing.systemdocs import (
18 LayeredDocFileSuite, setUp, tearDown)16 LayeredDocFileSuite, setUp, tearDown)
19from canonical.testing.layers import LaunchpadFunctionalLayer, MemcachedLayer17from canonical.testing.layers import LaunchpadFunctionalLayer, MemcachedLayer
20from lp.services.memcache.interfaces import IMemcacheClient
21from lp.services.testing import build_test_suite18from lp.services.testing import build_test_suite
22from lp.testing import TestCase
2319
2420
25here = os.path.dirname(os.path.realpath(__file__))21here = os.path.dirname(os.path.realpath(__file__))
@@ -59,11 +55,15 @@
59 test.globs['MemcachedLayer'] = MemcachedLayer55 test.globs['MemcachedLayer'] = MemcachedLayer
6056
6157
58def suite_for_doctest(filename):
59 return LayeredDocFileSuite(
60 '../doc/%s' % filename,
61 setUp=memcacheSetUp, tearDown=tearDown,
62 layer=LaunchpadFunctionalLayer)
63
62special = {64special = {
63 'tales-cache.txt': LayeredDocFileSuite(65 'tales-cache.txt': suite_for_doctest('tales-cache.txt'),
64 '../doc/tales-cache.txt',66 'restful-cache.txt': suite_for_doctest('restful-cache.txt'),
65 setUp=memcacheSetUp, tearDown=tearDown,
66 layer=LaunchpadFunctionalLayer),
67 }67 }
6868
6969
7070
=== modified file 'versions.cfg'
--- versions.cfg 2010-05-17 20:03:02 +0000
+++ versions.cfg 2010-06-01 20:53:29 +0000
@@ -28,7 +28,7 @@
28lazr.delegates = 1.1.028lazr.delegates = 1.1.0
29lazr.enum = 1.1.229lazr.enum = 1.1.2
30lazr.lifecycle = 1.130lazr.lifecycle = 1.1
31lazr.restful = 0.9.2631lazr.restful = 0.9.27
32lazr.restfulclient = 0.9.1432lazr.restfulclient = 0.9.14
33lazr.smtptest = 1.133lazr.smtptest = 1.1
34lazr.testing = 0.1.134lazr.testing = 0.1.1