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
47 """
48 Abstract class for cache implementations.
49 Main function just provides referenced api documentation.
50
51 Use CacheInRam or CacheOnDisk instead which are derived from this class.
52
53 Note:
54 Michele says: there are signatures inside gdbm files that are used
55 directly 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 """Initializes the object
70
71 Args:
72 request: the global request object
73 """
74 raise NotImplementedError
75
78 """
79 Tries to retrieve the value corresponding to `key` from the cache if the
80 object exists and if it did not expire, else it calls the function `f`
81 and stores the output in the cache corresponding to `key`. It always
82 returns the function that is returned.
83
84 Args:
85 key(str): the key of the object to be stored or retrieved
86 f(function): the function whose output is to be cached.
87
88 If `f` is `None` the cache is cleared.
89 time_expire(int): expiration of the cache in seconds.
90
91 It's used to compare the current time with the time
92 when the requested object was last saved in cache. It does not
93 affect future requests. Setting `time_expire` to 0 or negative
94 value forces the cache to refresh.
95 """
96 raise NotImplementedError
97
98 - def clear(self, regex=None):
99 """
100 Clears the cache of all keys that match the provided regular expression.
101 If no regular expression is provided, it clears all entries in cache.
102
103 Args:
104 regex: if provided, only keys matching the regex will be cleared,
105 otherwise all keys are cleared.
106 """
107
108 raise NotImplementedError
109
111 """
112 Increments the cached value for the given key by the amount in value
113
114 Args:
115 key(str): key for the cached object to be incremeneted
116 value(int): amount of the increment (defaults to 1, can be negative)
117 """
118 raise NotImplementedError
119
120 - def _clear(self, storage, regex):
121 """
122 Auxiliary function called by `clear` to search and clear cache entries
123 """
124 r = re.compile(regex)
125 for (key, value) in storage.items():
126 if r.match(str(key)):
127 del storage[key]
128
131 """
132 Ram based caching
133
134 This is implemented as global (per process, shared by all threads)
135 dictionary.
136 A mutex-lock mechanism avoid conflicts.
137 """
138
139 locker = thread.allocate_lock()
140 meta_storage = {}
141
146
164
165 - def clear(self, regex=None):
179
183 """
184 Attention! cache.ram does not copy the cached object.
185 It just stores a reference to it. Turns out the deepcopying the object
186 has some problems:
187
188 - would break backward compatibility
189 - would be limiting because people may want to cache live objects
190 - would work unless we deepcopy no storage and retrival which would make
191 things slow.
192
193 Anyway. You can deepcopy explicitly in the function generating the value
194 to be cached.
195 """
196 self.initialize()
197
198 dt = time_expire
199 now = time.time()
200
201 self.locker.acquire()
202 item = self.storage.get(key, None)
203 if item and f is None:
204 del self.storage[key]
205 if destroyer:
206 destroyer(item[1])
207 self.storage[CacheAbstract.cache_stats_name]['hit_total'] += 1
208 self.locker.release()
209
210 if f is None:
211 return None
212 if item and (dt is None or item[0] > now - dt):
213 return item[1]
214 elif item and (item[0] < now - dt) and destroyer:
215 destroyer(item[1])
216 value = f()
217
218 self.locker.acquire()
219 self.storage[key] = (now, value)
220 self.storage[CacheAbstract.cache_stats_name]['misses'] += 1
221 self.locker.release()
222 return value
223
236
239 """
240 Disk based cache
241
242 This is implemented as a shelve object and it is shared by multiple web2py
243 processes (and threads) as long as they share the same filesystem.
244 The file is locked when accessed.
245
246 Disk cache provides persistance when web2py is started/stopped but it slower
247 than `CacheInRam`
248
249 Values stored in disk cache must be pickable.
250 """
251
264
306
307 - def __init__(self, request=None, folder=None):
312
332
333 - def clear(self, regex=None):
344
370
382
384 - def __init__(self, func, key, time_expire, cache, cache_model):
385 self.__name__ = func.__name__
386 self.__doc__ = func.__doc__
387 self.func = func
388 self.key = key
389 self.time_expire = time_expire
390 self.cache = cache
391 self.cache_model = cache_model
392
394 if not self.key:
395 key2 = self.__name__ + ':' + repr(a) + ':' + repr(b)
396 else:
397 key2 = self.key.replace('%(name)s', self.__name__)\
398 .replace('%(args)s', str(a)).replace('%(vars)s', str(b))
399 cache_model = self.cache_model
400 if not cache_model or isinstance(cache_model, str):
401 cache_model = getattr(self.cache, cache_model or 'ram')
402 return cache_model(key2,
403 lambda a=a, b=b: self.func(*a, **b),
404 self.time_expire)
405
408 """
409 Sets up generic caching, creating an instance of both CacheInRam and
410 CacheOnDisk.
411 In case of GAE will make use of gluon.contrib.gae_memcache.
412
413 - self.ram is an instance of CacheInRam
414 - self.disk is an instance of CacheOnDisk
415 """
416
417 autokey = ':%(name)s:%(args)s:%(vars)s'
418
420 """
421 Args:
422 request: the global request object
423 """
424
425 if have_settings and settings.global_settings.web2py_runtime_gae:
426 from gluon.contrib.gae_memcache import MemcacheClient
427 self.ram = self.disk = MemcacheClient(request)
428 else:
429
430 self.ram = CacheInRam(request)
431 try:
432 self.disk = CacheOnDisk(request)
433 except IOError:
434 logger.warning('no cache.disk (IOError)')
435 except AttributeError:
436
437
438 logger.warning('no cache.disk (AttributeError)')
439
440 - def action(self, time_expire=DEFAULT_TIME_EXPIRE, cache_model=None,
441 prefix=None, session=False, vars=True, lang=True,
442 user_agent=False, public=True, valid_statuses=None,
443 quick=None):
444 """Better fit for caching an action
445
446 Warning:
447 Experimental!
448
449 Currently only HTTP 1.1 compliant
450 reference : http://code.google.com/p/doctype-mirror/wiki/ArticleHttpCaching
451
452 Args:
453 time_expire(int): same as @cache
454 cache_model(str): same as @cache
455 prefix(str): add a prefix to the calculated key
456 session(bool): adds response.session_id to the key
457 vars(bool): adds request.env.query_string
458 lang(bool): adds T.accepted_language
459 user_agent(bool or dict): if True, adds is_mobile and is_tablet to the key.
460 Pass a dict to use all the needed values (uses str(.items()))
461 (e.g. user_agent=request.user_agent()). Used only if session is
462 not True
463 public(bool): if False forces the Cache-Control to be 'private'
464 valid_statuses: by default only status codes starting with 1,2,3 will be cached.
465 pass an explicit list of statuses on which turn the cache on
466 quick: Session,Vars,Lang,User-agent,Public:
467 fast overrides with initials, e.g. 'SVLP' or 'VLP', or 'VLP'
468 """
469 from gluon import current
470 from gluon.http import HTTP
471 def wrap(func):
472 def wrapped_f():
473 if current.request.env.request_method != 'GET':
474 return func()
475 if time_expire:
476 cache_control = 'max-age=%(time_expire)s, s-maxage=%(time_expire)s' % dict(time_expire=time_expire)
477 if quick:
478 session_ = True if 'S' in quick else False
479 vars_ = True if 'V' in quick else False
480 lang_ = True if 'L' in quick else False
481 user_agent_ = True if 'U' in quick else False
482 public_ = True if 'P' in quick else False
483 else:
484 session_, vars_, lang_, user_agent_, public_ = session, vars, lang, user_agent, public
485 if not session_ and public_:
486 cache_control += ', public'
487 expires = (current.request.utcnow + datetime.timedelta(seconds=time_expire)).strftime('%a, %d %b %Y %H:%M:%S GMT')
488 else:
489 cache_control += ', private'
490 expires = 'Fri, 01 Jan 1990 00:00:00 GMT'
491 if cache_model:
492
493 cache_key = [current.request.env.path_info, current.response.view]
494 if session_:
495 cache_key.append(current.response.session_id)
496 elif user_agent_:
497 if user_agent_ is True:
498 cache_key.append("%(is_mobile)s_%(is_tablet)s" % current.request.user_agent())
499 else:
500 cache_key.append(str(user_agent_.items()))
501 if vars_:
502 cache_key.append(current.request.env.query_string)
503 if lang_:
504 cache_key.append(current.T.accepted_language)
505 cache_key = hashlib.md5('__'.join(cache_key)).hexdigest()
506 if prefix:
507 cache_key = prefix + cache_key
508 try:
509
510 rtn = cache_model(cache_key, lambda : func(), time_expire=time_expire)
511 http, status = None, current.response.status
512 except HTTP, e:
513
514 rtn = cache_model(cache_key, lambda : e.body, time_expire=time_expire)
515 http, status = HTTP(e.status, rtn, **e.headers), e.status
516 else:
517
518 http = None
519 else:
520
521 try:
522
523 rtn = func()
524 http, status = None, current.response.status
525 except HTTP, e:
526
527 status = e.status
528 http = HTTP(e.status, e.body, **e.headers)
529 else:
530
531 http = None
532 send_headers = False
533 if http and isinstance(valid_statuses, list):
534 if status in valid_statuses:
535 send_headers = True
536 elif valid_statuses is None:
537 if str(status)[0] in '123':
538 send_headers = True
539 if send_headers:
540 headers = {
541 'Pragma' : None,
542 'Expires' : expires,
543 'Cache-Control' : cache_control
544 }
545 current.response.headers.update(headers)
546 if cache_model and not send_headers:
547
548
549 cache_model(cache_key, None)
550 if http:
551 if send_headers:
552 http.headers.update(current.response.headers)
553 raise http
554 return rtn
555 wrapped_f.__name__ = func.__name__
556 wrapped_f.__doc__ = func.__doc__
557 return wrapped_f
558 return wrap
559
564 """
565 Decorator function that can be used to cache any function/method.
566
567 Args:
568 key(str) : the key of the object to be store or retrieved
569 time_expire(int) : expiration of the cache in seconds
570 `time_expire` is used to compare the current time with the time
571 when the requested object was last saved in cache.
572 It does not affect future requests.
573 Setting `time_expire` to 0 or negative value forces the cache to
574 refresh.
575 cache_model(str): can be "ram", "disk" or other (like "memcache").
576 Defaults to "ram"
577
578 When the function `f` is called, web2py tries to retrieve
579 the value corresponding to `key` from the cache if the
580 object exists and if it did not expire, else it calles the function `f`
581 and stores the output in the cache corresponding to `key`. In the case
582 the output of the function is returned.
583
584 Example: ::
585
586 @cache('key', 5000, cache.ram)
587 def f():
588 return time.ctime()
589
590 Note:
591 If the function `f` is an action, we suggest using
592 @cache.action instead
593 """
594
595 def tmp(func, cache=self, cache_model=cache_model):
596 return CacheAction(func, key, time_expire, self, cache_model)
597 return tmp
598
599 @staticmethod
601 """
602 allow replacing cache.ram with cache.with_prefix(cache.ram,'prefix')
603 it will add prefix to all the cache keys used.
604 """
605 return lambda key, f, time_expire=DEFAULT_TIME_EXPIRE, prefix=prefix:\
606 cache_model(prefix + key, f, time_expire)
607
608
609 -def lazy_cache(key=None, time_expire=None, cache_model='ram'):
610 """
611 Can be used to cache any function including ones in modules,
612 as long as the cached function is only called within a web2py request
613
614 If a key is not provided, one is generated from the function name
615 `time_expire` defaults to None (no cache expiration)
616
617 If cache_model is "ram" then the model is current.cache.ram, etc.
618 """
619 def decorator(f, key=key, time_expire=time_expire, cache_model=cache_model):
620 key = key or repr(f)
621
622 def g(*c, **d):
623 from gluon import current
624 return current.cache(key, time_expire, cache_model)(f)(*c, **d)
625 g.__name__ = f.__name__
626 return g
627 return decorator
628