1
2
3
4 """
5 This file is part of the web2py Web Framework
6 Copyrighted by Massimo Di Pierro <mdipierro@cs.depaul.edu>
7 License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html)
8
9 Basic caching classes and methods
10 =================================
11
12 - Cache - The generic caching object interfacing with the others
13 - CacheInRam - providing caching in ram
14 - CacheOnDisk - provides caches on disk
15
16 Memcache is also available via a different module (see gluon.contrib.memcache)
17
18 When web2py is running on Google App Engine,
19 caching will be provided by the GAE memcache
20 (see gluon.contrib.gae_memcache)
21 """
22 import time
23 import portalocker
24 import shelve
25 import thread
26 import os
27 import logging
28 import re
29 import hashlib
30 import datetime
31 try:
32 from gluon import settings
33 have_settings = True
34 except ImportError:
35 have_settings = False
36
37 logger = logging.getLogger("web2py.cache")
38
39 __all__ = ['Cache', 'lazy_cache']
40
41
42 DEFAULT_TIME_EXPIRE = 300
46 """
47 Abstract class for cache implementations.
48 Main function is now to provide referenced api documentation.
49
50 Use CacheInRam or CacheOnDisk instead which are derived from this class.
51
52 Attentions, Michele says:
53
54 There are signatures inside gdbm files that are used directly
55 by the python gdbm adapter that often are lagging behind in the
56 detection code in python part.
57 On every occasion that a gdbm store is probed by the python adapter,
58 the probe fails, because gdbm file version is newer.
59 Using gdbm directly from C would work, because there is backward
60 compatibility, but not from python!
61 The .shelve file is discarded and a new one created (with new
62 signature) and it works until it is probed again...
63 The possible consequences are memory leaks and broken sessions.
64 """
65
66 cache_stats_name = 'web2py_cache_statistics'
67
69 """
70 Parameters
71 ----------
72 request:
73 the global request object
74 """
75 raise NotImplementedError
76
79 """
80 Tries retrieve the value corresponding to `key` from the cache of the
81 object exists and if it did not expire, else it called the function `f`
82 and stores the output in the cache corresponding to `key`. In the case
83 the output of the function is returned.
84
85 :param key: the key of the object to be store or retrieved
86 :param f: the function, whose output is to be cached
87 :param time_expire: expiration of the cache in microseconds
88
89 - `time_expire` is used to compare the current time with the time when
90 the requested object was last saved in cache. It does not affect
91 future requests.
92 - Setting `time_expire` to 0 or negative value forces the cache to
93 refresh.
94
95 If the function `f` is `None` the cache is cleared.
96 """
97 raise NotImplementedError
98
99 - def clear(self, regex=None):
100 """
101 Clears the cache of all keys that match the provided regular expression.
102 If no regular expression is provided, it clears all entries in cache.
103
104 Parameters
105 ----------
106 regex:
107 if provided, only keys matching the regex will be cleared.
108 Otherwise all keys are cleared.
109 """
110
111 raise NotImplementedError
112
114 """
115 Increments the cached value for the given key by the amount in value
116
117 Parameters
118 ----------
119 key:
120 key for the cached object to be incremeneted
121 value:
122 amount of the increment (defaults to 1, can be negative)
123 """
124 raise NotImplementedError
125
126 - def _clear(self, storage, regex):
127 """
128 Auxiliary function called by `clear` to search and clear cache entries
129 """
130 r = re.compile(regex)
131 for (key, value) in storage.items():
132 if r.match(str(key)):
133 del storage[key]
134
137 """
138 Ram based caching
139
140 This is implemented as global (per process, shared by all threads)
141 dictionary.
142 A mutex-lock mechanism avoid conflicts.
143 """
144
145 locker = thread.allocate_lock()
146 meta_storage = {}
147
152
170
171 - def clear(self, regex=None):
185
189 """
190 Attention! cache.ram does not copy the cached object. It just stores a reference to it.
191 Turns out the deepcopying the object has some problems:
192 1) would break backward compatibility
193 2) would be limiting because people may want to cache live objects
194 3) would work unless we deepcopy no storage and retrival which would make things slow.
195 Anyway. You can deepcopy explicitly in the function generating the value to be cached.
196 """
197 self.initialize()
198
199 dt = time_expire
200 now = time.time()
201
202 self.locker.acquire()
203 item = self.storage.get(key, None)
204 if item and f is None:
205 del self.storage[key]
206 if destroyer:
207 destroyer(item[1])
208 self.storage[CacheAbstract.cache_stats_name]['hit_total'] += 1
209 self.locker.release()
210
211 if f is None:
212 return None
213 if item and (dt is None or item[0] > now - dt):
214 return item[1]
215 elif item and (item[0] < now - dt) and destroyer:
216 destroyer(item[1])
217 value = f()
218
219 self.locker.acquire()
220 self.storage[key] = (now, value)
221 self.storage[CacheAbstract.cache_stats_name]['misses'] += 1
222 self.locker.release()
223 return value
224
237
240 """
241 Disk based cache
242
243 This is implemented as a shelve object and it is shared by multiple web2py
244 processes (and threads) as long as they share the same filesystem.
245 The file is locked when accessed.
246
247 Disk cache provides persistance when web2py is started/stopped but it slower
248 than `CacheInRam`
249
250 Values stored in disk cache must be pickable.
251 """
252
262
303
304 - def __init__(self, request=None, folder=None):
309
329
330 - def clear(self, regex=None):
341
367
379
381 - def __init__(self, func, key, time_expire, cache, cache_model):
382 self.__name__ = func.__name__
383 self.__doc__ = func.__doc__
384 self.func = func
385 self.key = key
386 self.time_expire = time_expire
387 self.cache = cache
388 self.cache_model = cache_model
389
391 if not self.key:
392 key2 = self.__name__ + ':' + repr(a) + ':' + repr(b)
393 else:
394 key2 = self.key.replace('%(name)s', self.__name__)\
395 .replace('%(args)s', str(a)).replace('%(vars)s', str(b))
396 cache_model = self.cache_model
397 if not cache_model or isinstance(cache_model, str):
398 cache_model = getattr(self.cache, cache_model or 'ram')
399 return cache_model(key2,
400 lambda a=a, b=b: self.func(*a, **b),
401 self.time_expire)
402
405 """
406 Sets up generic caching, creating an instance of both CacheInRam and
407 CacheOnDisk.
408 In case of GAE will make use of gluon.contrib.gae_memcache.
409
410 - self.ram is an instance of CacheInRam
411 - self.disk is an instance of CacheOnDisk
412 """
413
414 autokey = ':%(name)s:%(args)s:%(vars)s'
415
417 """
418 Parameters
419 ----------
420 request:
421 the global request object
422 """
423
424 if have_settings and settings.global_settings.web2py_runtime_gae:
425 from gluon.contrib.gae_memcache import MemcacheClient
426 self.ram = self.disk = MemcacheClient(request)
427 else:
428
429 self.ram = CacheInRam(request)
430 try:
431 self.disk = CacheOnDisk(request)
432 except IOError:
433 logger.warning('no cache.disk (IOError)')
434 except AttributeError:
435
436
437 logger.warning('no cache.disk (AttributeError)')
438
439 - def action(self, time_expire=DEFAULT_TIME_EXPIRE, cache_model=None,
440 prefix=None, session=False, vars=True, lang=True,
441 user_agent=False, public=True, valid_statuses=None,
442 quick=None):
443 """
444 Experimental!
445 Currently only HTTP 1.1 compliant
446 reference : http://code.google.com/p/doctype-mirror/wiki/ArticleHttpCaching
447 time_expire: same as @cache
448 cache_model: same as @cache
449 prefix: add a prefix to the calculated key
450 session: adds response.session_id to the key
451 vars: adds request.env.query_string
452 lang: adds T.accepted_language
453 user_agent: if True, adds is_mobile and is_tablet to the key.
454 Pass a dict to use all the needed values (uses str(.items())) (e.g. user_agent=request.user_agent())
455 used only if session is not True
456 public: if False forces the Cache-Control to be 'private'
457 valid_statuses: by default only status codes starting with 1,2,3 will be cached.
458 pass an explicit list of statuses on which turn the cache on
459 quick: Session,Vars,Lang,User-agent,Public:
460 fast overrides with initial strings, e.g. 'SVLP' or 'VLP', or 'VLP'
461 """
462 from gluon import current
463 from gluon.http import HTTP
464 def wrap(func):
465 def wrapped_f():
466 if current.request.env.request_method != 'GET':
467 return func()
468 if time_expire:
469 cache_control = 'max-age=%(time_expire)s, s-maxage=%(time_expire)s' % dict(time_expire=time_expire)
470 if quick:
471 session_ = True if 'S' in quick else False
472 vars_ = True if 'V' in quick else False
473 lang_ = True if 'L' in quick else False
474 user_agent_ = True if 'U' in quick else False
475 public_ = True if 'P' in quick else False
476 else:
477 session_, vars_, lang_, user_agent_, public_ = session, vars, lang, user_agent, public
478 if not session_ and public_:
479 cache_control += ', public'
480 expires = (current.request.utcnow + datetime.timedelta(seconds=time_expire)).strftime('%a, %d %b %Y %H:%M:%S GMT')
481 else:
482 cache_control += ', private'
483 expires = 'Fri, 01 Jan 1990 00:00:00 GMT'
484 if cache_model:
485
486 cache_key = [current.request.env.path_info, current.response.view]
487 if session_:
488 cache_key.append(current.response.session_id)
489 elif user_agent_:
490 if user_agent_ is True:
491 cache_key.append("%(is_mobile)s_%(is_tablet)s" % current.request.user_agent())
492 else:
493 cache_key.append(str(user_agent_.items()))
494 if vars_:
495 cache_key.append(current.request.env.query_string)
496 if lang_:
497 cache_key.append(current.T.accepted_language)
498 cache_key = hashlib.md5('__'.join(cache_key)).hexdigest()
499 if prefix:
500 cache_key = prefix + cache_key
501 try:
502
503 rtn = cache_model(cache_key, lambda : func(), time_expire=time_expire)
504 http, status = None, current.response.status
505 except HTTP, e:
506
507 rtn = cache_model(cache_key, lambda : e.body, time_expire=time_expire)
508 http, status = HTTP(e.status, rtn, **e.headers), e.status
509 else:
510
511 http = None
512 else:
513
514 try:
515
516 rtn = func()
517 http, status = None, current.response.status
518 except HTTP, e:
519
520 status = e.status
521 http = HTTP(e.status, e.body, **e.headers)
522 else:
523
524 http = None
525 send_headers = False
526 if http and isinstance(valid_statuses, list):
527 if status in valid_statuses:
528 send_headers = True
529 elif valid_statuses is None:
530 if str(status)[0] in '123':
531 send_headers = True
532 if send_headers:
533 headers = {
534 'Pragma' : None,
535 'Expires' : expires,
536 'Cache-Control' : cache_control
537 }
538 current.response.headers.update(headers)
539 if cache_model and not send_headers:
540
541
542 cache_model(cache_key, None)
543 if http:
544 if send_headers:
545 http.headers.update(current.response.headers)
546 raise http
547 return rtn
548 wrapped_f.__name__ = func.__name__
549 wrapped_f.__doc__ = func.__doc__
550 return wrapped_f
551 return wrap
552
557 """
558 Decorator function that can be used to cache any function/method.
559
560 Example::
561
562 @cache('key', 5000, cache.ram)
563 def f():
564 return time.ctime()
565
566 When the function f is called, web2py tries to retrieve
567 the value corresponding to `key` from the cache of the
568 object exists and if it did not expire, else it calles the function `f`
569 and stores the output in the cache corresponding to `key`. In the case
570 the output of the function is returned.
571
572 :param key: the key of the object to be store or retrieved
573 :param time_expire: expiration of the cache in microseconds
574 :param cache_model: "ram", "disk", or other
575 (like "memcache" if defined). It defaults to "ram".
576
577 Notes
578 -----
579 `time_expire` is used to compare the curret time with the time when the
580 requested object was last saved in cache. It does not affect future
581 requests.
582 Setting `time_expire` to 0 or negative value forces the cache to
583 refresh.
584
585 If the function `f` is an action, we suggest using
586 @cache.client instead
587 """
588
589 def tmp(func, cache=self, cache_model=cache_model):
590 return CacheAction(func, key, time_expire, self, cache_model)
591 return tmp
592
593 @staticmethod
595 """
596 allow replacing cache.ram with cache.with_prefix(cache.ram,'prefix')
597 it will add prefix to all the cache keys used.
598 """
599 return lambda key, f, time_expire=DEFAULT_TIME_EXPIRE, prefix=prefix:\
600 cache_model(prefix + key, f, time_expire)
601
602
603 -def lazy_cache(key=None, time_expire=None, cache_model='ram'):
604 """
605 can be used to cache any function including in modules,
606 as long as the cached function is only called within a web2py request
607 if a key is not provided, one is generated from the function name
608 the time_expire defaults to None (no cache expiration)
609 if cache_model is "ram" then the model is current.cache.ram, etc.
610 """
611 def decorator(f, key=key, time_expire=time_expire, cache_model=cache_model):
612 key = key or repr(f)
613
614 def g(*c, **d):
615 from gluon import current
616 return current.cache(key, time_expire, cache_model)(f)(*c, **d)
617 g.__name__ = f.__name__
618 return g
619 return decorator
620