cache

cache is a global object that is also available in the web2py execution environment. It has two attributes:

  • cache.ram: the application cache in main memory.
  • cache.disk: the application cache on disk.

cache is callable, this allows it to be used as a decorator for caching actions and views.

The following example caches the time.ctime() function in RAM:

  1. def cache_in_ram():
  2. import time
  3. t = cache.ram('time', lambda: time.ctime(), time_expire=5)
  4. return dict(time=t, link=A('click me', _href=request.url))

The output of lambda: time.ctime() is cached in RAM for 5 seconds. The string 'time' is used as cache key.

The following example caches the time.ctime() function on disk:

  1. def cache_on_disk():
  2. import time
  3. t = cache.disk('time', lambda: time.ctime(), time_expire=5)
  4. return dict(time=t, link=A('click me', _href=request.url))

The output of lambda: time.ctime() is cached on disk for 5 seconds.

Note, the second argument to cache.ram and cache.disk must be a function or callable object. If you want to cache an existing object rather than the output of a function, you can simply return it via a lambda function:

  1. cache.ram('myobject', lambda: myobject, time_expire=60*60*24)

The next example caches the time.ctime() function to both RAM and disk:

  1. def cache_in_ram_and_disk():
  2. import time
  3. t = cache.ram('time', lambda: cache.disk('time', lambda: time.ctime(), time_expire=5), time_expire=5)
  4. return dict(time=t, link=A('click me', _href=request.url))

The output of lambda: time.ctime() is cached on disk and then in RAM for 5 seconds. web2py looks in RAM first and if not there it looks on disk. If it is not in RAM or on disk, lambda: time.ctime() is executed and the cache is updated. This technique is useful in a multiprocessor environment. The two times do not have to be the same.

The following example is caching in RAM the output of the controller function (but not the view):

  1. @cache(request.env.path_info, time_expire=5, cache_model=cache.ram)
  2. def cache_controller_in_ram():
  3. import time
  4. t = time.ctime()
  5. return dict(time=t, link=A('click me', _href=request.url))

The dictionary returned by cache_controller_in_ram is cached in RAM for 5 seconds. Note that the result of a database select cannot be cached without first being serialized. A better way is to cache the database select directly using the select method’s cache argument.

The following example is caching the output of the controller function on disk (but not the view):

  1. @cache(request.env.path_info, time_expire=5, cache_model=cache.disk)
  2. def cache_controller_on_disk():
  3. import time
  4. t = time.ctime()
  5. return dict(time=t, link=A('click to reload', _href=request.url))

The dictionary returned by cache_controller_on_disk is cached on disk for 5 seconds. Remember that web2py cannot cache a dictionary that contains un-pickleable objects.

It is also possible to cache the view. The trick is to render the view in the controller function, so that the controller returns a string. This is done by returning response.render(d) where d is the dictionary we intended to pass to the view. The following example caches the output of the controller function in RAM (including the rendered view):

  1. @cache(request.env.path_info, time_expire=5, cache_model=cache.ram)
  2. def cache_controller_and_view():
  3. import time
  4. t = time.ctime()
  5. d = dict(time=t, link=A('click to reload', _href=request.url))
  6. return response.render(d)

response.render(d) returns the rendered view as a string, which is now cached for 5 seconds. This is the best and fastest way of caching.

We recommend @cache.action starting from web2py 2.4.7+

Note, time_expire is used to compare the current time with the time the requested object was last saved in the cache. It does not affect future requests. This enables time_expire to be set dynamically when an object is requested rather than being fixed when the object is saved. For example:

  1. message = cache.ram('message', lambda: 'Hello', time_expire=5)

Now, suppose the following call is made 10 seconds after the above call:

  1. message = cache.ram('message', lambda: 'Goodbye', time_expire=20)

Because time_expire is set to 20 seconds in the second call and only 10 seconds has elapsed since the message was first saved, the value “Hello” will be retrieved from the cache, and it will not be updated with “Goodbye”. The time_expire value of 5 seconds in the first call has no impact on the second call.

Setting time_expire=0 (or a negative value) forces the cached item to be refreshed (because the elapsed time since the last save will always be > 0), and setting time_expire=None forces retrieval of the cached value, regardless of the time elapsed since it was saved (if time_expire is always None, the cached item will effectively never expire).

You can clear one or more cache variables with

  1. cache.ram.clear(regex='...')

where regex is a regular expression matching all the keys you want removed from the cache. You can also clear a single item with:

  1. cache.ram(key, None)

where key is the key of the cached item.

It is also possible to define other caching mechanisms such as memcache. Memcache is available via gluon.contrib.memcache and is discussed in more detail in Chapter 13.

Be careful when caching to remember that caching is usually at the app-level not at the user level. If you need, for example, to cache user specific content, choose a key that includes the user id.

The admin app for an application lets you view cache keys (and clear the cache). Access it from the database management screen of admin.

cache.action

Web2py by default assumes that the returned content is not going to be cached, as this reduces the shortcomings of an improper caching of the page client-side.

For example, when you show a form to the user, or a list of records, the web page should not be cached, as other users may have inserted new records on the table you are showing.

Instead, if you are showing to the user a wiki page whose content will never change (or it changes once a week), it is useful to store that page, but it is even more useful to tell the client that that page is not going to change.

This is accomplished sending out some specific headers along with the page: when the client’s browser receives the content, it is stored in the browser’s cache and it will not be requested again to your site. Of course this is a major speedup for public-facing sites.

Web2py 2.4.7+ introduced a new cache.action decorator to allow a smarter handling of this situation. cache.action can be used:

  • for setting smart cache headers
  • to cache the results accordingly

NB: it will do one or another or both.

The main problem with caching a view with @cache(request.env.path_info, time_expire=300, cache_model=cache.ram) is that request.env.path_info as a key leads to several problems, e.g.

  1. URL vars are not considered
    • You cached the result of /app/default/index?search=foo : for the next 300 seconds /app/default/index?search=bar will return the exact same thing of /app/default/index?search=foo
  2. User is not considered
    • Your user accesses a page often and you choose to cache it. However, you cached the result of /app/default/index using request.env.path_info as the key, so another user will see a page that was not meant for him
    • You cached a page for “Bill”, but “Bill” accessed the page from the desktop. Now he tries to access it from his phone: if you prepared a template for mobile users that is different from the standard one, “Joe” will not see it
  3. Language is not considered
    • When you cache the page, if you use T() for some elements, the page will be stored with a fixed translation
  4. Method is not considered
    • When you cache a page, you should only cache it if it’s a result of a GET operation
  5. Status code is not considered
    • When you cached the page for the first time, something went wrong and you returned a nice 404 page. You don’t want to cache errors ^_^

Instead of letting users write a lot of boilerplate code to take care of all those problems, cache.action was created. It will by default use smart cache headers to let the browser cache the result: if you pass a cache model to it, it will also figure out the best key automatically, so different versions of the same page can be stored and retrieved accordingly (e.g. one for English users and one for Spanish ones)

It takes several parameters, with smart defaults:

  • time_expire : the usual, defaults to 300 seconds
  • cache_model : by default is None. This means that @cache.action will only alter the default headers to let the client’s browser cache the content
    • if you pass, e.g., cache.ram, the result will be stored in the cache as well
  • prefix : if you want to prefix the auto-generated key (useful for clearing it later with, e.g. cache.ram.clear(prefix*))
  • session : if you want to consider the session, defaults to False
  • vars : if you want to consider URL vars, defaults to True
  • lang : if you want to consider the language, defaults to True
  • user_agent : if you want to consider the user agent, defaults to False
  • public : if you want the same page for all the users that will ever access it, defaults to True
  • valid_statuses : defaults to None. @cache.action will cache only pages requested with a GET method, whose status codes begin with 1,2 or 3. You can pass a list of status codes (when you want pages to be cached with those statuses, e.g. status_codes=[200] will cache only pages that resulted in a 200 status code)
  • quick : defaults to None, but you can pass a list of initials to set a particular feature:
    • Session, Vars, Lang, User_agent, Public e.g. @cache.action(time_expire=300, cache_model=cache.ram, quick='SVP') is the same as @cache.action(time_expire=300, cache_model=cache.ram, session=True, vars=True, public=True)

“Consider” means for e.g. vars, that you want to cache different pages if vars are different, so /app/default/index?search=foo will not be the same one for /app/default/index?search=bar Some settings override others, so, e.g., if you set session=True, public=True the latter will be discarded. Use them wisely!