better cache-disk, thanks Leonel

This commit is contained in:
mdipierro
2014-09-04 22:27:52 -05:00
parent 9fd827c561
commit 2d9f0fafdc
3 changed files with 665 additions and 638 deletions
+9 -9
View File
@@ -35,15 +35,15 @@ src:
make clean make clean
### clean up baisc apps ### clean up baisc apps
rm -f routes.py rm -f routes.py
rm -f applications/*/sessions/* rm -rf applications/*/sessions/*
rm -f applications/*/errors/* | echo 'too many files' rm -rf applications/*/errors/* | echo 'too many files'
rm -f applications/*/cache/* rm -rf applications/*/cache/*
rm -f applications/admin/databases/* rm -rf applications/admin/databases/*
rm -f applications/welcome/databases/* rm -rf applications/welcome/databases/*
rm -f applications/examples/databases/* rm -rf applications/examples/databases/*
rm -f applications/admin/uploads/* rm -rf applications/admin/uploads/*
rm -f applications/welcome/uploads/* rm -rf applications/welcome/uploads/*
rm -f applications/examples/uploads/* rm -rf applications/examples/uploads/*
### NO MORE make epydoc ### NO MORE make epydoc
# make epydoc # make epydoc
### make welcome layout and appadmin the default ### make welcome layout and appadmin the default
+1 -1
View File
@@ -1 +1 @@
Version 2.9.6-stable+timestamp.2014.09.04.22.16.03 Version 2.9.6-stable+timestamp.2014.09.04.22.27.48
+119 -92
View File
@@ -20,20 +20,27 @@ caching will be provided by the GAE memcache
(see gluon.contrib.gae_memcache) (see gluon.contrib.gae_memcache)
""" """
import time import time
import portalocker import shutil
import shelve
import thread import thread
import os import os
import sys
import logging import logging
import re import re
import hashlib import hashlib
import datetime import datetime
import tempfile
from gluon import recfile
try: try:
from gluon import settings from gluon import settings
have_settings = True have_settings = True
except ImportError: except ImportError:
have_settings = False have_settings = False
try:
import cPickle as pickle
except:
import pickle
logger = logging.getLogger("web2py.cache") logger = logging.getLogger("web2py.cache")
__all__ = ['Cache', 'lazy_cache'] __all__ = ['Cache', 'lazy_cache']
@@ -122,7 +129,7 @@ class CacheAbstract(object):
Auxiliary function called by `clear` to search and clear cache entries Auxiliary function called by `clear` to search and clear cache entries
""" """
r = re.compile(regex) r = re.compile(regex)
for (key, value) in storage.items(): for key in storage:
if r.match(str(key)): if r.match(str(key)):
del storage[key] del storage[key]
break break
@@ -242,7 +249,6 @@ class CacheOnDisk(CacheAbstract):
This is implemented as a shelve object and it is shared by multiple web2py This is implemented as a shelve object and it is shared by multiple web2py
processes (and threads) as long as they share the same filesystem. processes (and threads) as long as they share the same filesystem.
The file is locked when accessed.
Disk cache provides persistance when web2py is started/stopped but it slower Disk cache provides persistance when web2py is started/stopped but it slower
than `CacheInRam` than `CacheInRam`
@@ -250,137 +256,158 @@ class CacheOnDisk(CacheAbstract):
Values stored in disk cache must be pickable. Values stored in disk cache must be pickable.
""" """
def _close_shelve_and_unlock(self): class PersistentStorage(object):
"""
Implements a key based storage in disk.
"""
def __init__(self, folder):
self.folder = folder
# Check the best way to do atomic file replacement.
if sys.version_info >= (3, 3):
self.replace = os.replace
elif sys.platform == "win32":
import ctypes
from ctypes import wintypes
ReplaceFile = ctypes.windll.kernel32.ReplaceFileW
ReplaceFile.restype = wintypes.BOOL
ReplaceFile.argtypes = [
wintypes.LPWSTR,
wintypes.LPWSTR,
wintypes.LPWSTR,
wintypes.DWORD,
wintypes.LPVOID,
wintypes.LPVOID,
]
def replace_windows(src, dst):
if not ReplaceFile(dst, src, None, 0, 0, 0):
os.rename(src, dst)
self.replace = replace_windows
else:
# POSIX rename() is always atomic
self.replace = os.rename
def __setitem__(self, key, value):
tmp_name, tmp_path = tempfile.mkstemp(dir=self.folder)
tmp = os.fdopen(tmp_name, 'wb')
try: try:
if self.storage: pickle.dump((time.time(), value), tmp, pickle.HIGHEST_PROTOCOL)
self.storage.close()
except ValueError:
pass
finally: finally:
self.storage = None tmp.close()
if self.locker and self.locked: fullfilename = os.path.join(self.folder, recfile.generate(key))
portalocker.unlock(self.locker) if not os.path.exists(os.path.dirname(fullfilename)):
self.locker.close() os.makedirs(os.path.dirname(fullfilename))
self.locked = False self.replace(tmp_path, fullfilename)
def _open_shelve_and_lock(self):
"""Open and return a shelf object, obtaining an exclusive lock
on self.locker first. Replaces the close method of the
returned shelf instance with one that releases the lock upon
closing."""
storage = None def __getitem__(self, key):
locker = None if recfile.exists(key, path=self.folder):
locked = False timestamp, value = pickle.load(recfile.open(key, 'rb', path=self.folder))
return value
else:
raise KeyError
def __contains__(self, key):
return recfile.exists(key, path=self.folder)
def __delitem__(self, key):
recfile.remove(key, path=self.folder)
def __iter__(self):
for dirpath, dirnames, filenames in os.walk(self.folder):
for filename in filenames:
yield filename
def get(self, key, default=None):
try: try:
locker = locker = open(self.locker_name, 'a') return self[key]
portalocker.lock(locker, portalocker.LOCK_EX) except KeyError:
locked = True return default
try:
storage = shelve.open(self.shelve_name)
except: def clear(self):
logger.error('corrupted cache file %s, will try rebuild it' for key in self:
% self.shelve_name) del self[key]
storage = None
if storage is None:
if os.path.exists(self.shelve_name):
os.unlink(self.shelve_name)
storage = shelve.open(self.shelve_name)
if not CacheAbstract.cache_stats_name in storage.keys():
storage[CacheAbstract.cache_stats_name] = {
'hit_total': 0, 'misses': 0}
storage.sync()
except Exception, e:
if storage:
storage.close()
storage = None
if locked:
portalocker.unlock(locker)
locker.close()
locked = False
raise RuntimeError(
'unable to create/re-create cache file %s' % self.shelve_name)
self.locker = locker
self.locked = locked
self.storage = storage
return storage
def __init__(self, request=None, folder=None): def __init__(self, request=None, folder=None):
self.initialized = False self.initialized = False
self.request = request self.request = request
self.folder = folder self.folder = folder
self.storage = {} self.storage = None
def initialize(self): def initialize(self):
if self.initialized: if self.initialized:
return return
else: else:
self.initialized = True self.initialized = True
folder = self.folder folder = self.folder
request = self.request request = self.request
# Lets test if the cache folder exists, if not # Lets test if the cache folder exists, if not
# we are going to create it # we are going to create it
folder = folder or os.path.join(request.folder, 'cache') folder = os.path.join(folder or request.folder, 'cache')
if not os.path.exists(folder): if not os.path.exists(folder):
os.mkdir(folder) os.mkdir(folder)
### we need this because of a possible bug in shelve that may self.storage = CacheOnDisk.PersistentStorage(folder)
### or may not lock
self.locker_name = os.path.join(folder, 'cache.lock') if not CacheAbstract.cache_stats_name in self.storage:
self.shelve_name = os.path.join(folder, 'cache.shelve') self.storage[CacheAbstract.cache_stats_name] = {'hit_total': 0, 'misses': 0}
def clear(self, regex=None):
self.initialize()
storage = self._open_shelve_and_lock()
try:
if regex is None:
storage.clear()
else:
self._clear(storage, regex)
storage.sync()
finally:
self._close_shelve_and_unlock()
def __call__(self, key, f, def __call__(self, key, f,
time_expire=DEFAULT_TIME_EXPIRE): time_expire=DEFAULT_TIME_EXPIRE):
self.initialize() self.initialize()
dt = time_expire dt = time_expire
storage = self._open_shelve_and_lock() item = self.storage.get(key)
try: self.storage[CacheAbstract.cache_stats_name]['hit_total'] += 1
item = storage.get(key, None)
storage[CacheAbstract.cache_stats_name]['hit_total'] += 1
if item and f is None: if item and f is None:
del storage[key] del self.storage[key]
storage.sync()
now = time.time()
if f is None: if f is None:
value = None return None
elif item and (dt is None or item[0] > now - dt):
now = time.time()
if item and ((dt is None) or (item[0] > now - dt)):
value = item[1] value = item[1]
else: else:
value = f() value = f()
storage[key] = (now, value) self.storage[key] = (now, value)
storage[CacheAbstract.cache_stats_name]['misses'] += 1 self.storage[CacheAbstract.cache_stats_name]['misses'] += 1
storage.sync()
finally:
self._close_shelve_and_unlock()
return value return value
def clear(self, regex=None):
self.initialize()
storage = self.storage
if regex is None:
storage.clear()
else:
self._clear(storage, regex)
if not CacheAbstract.cache_stats_name in storage:
storage[CacheAbstract.cache_stats_name] = {
'hit_total': 0, 'misses': 0}
def increment(self, key, value=1): def increment(self, key, value=1):
self.initialize() self.initialize()
storage = self._open_shelve_and_lock() self.storage[key] += value
try:
if key in storage:
value = storage[key][1] + value
storage[key] = (time.time(), value)
storage.sync()
finally:
self._close_shelve_and_unlock()
return value return value
class CacheAction(object): class CacheAction(object):
def __init__(self, func, key, time_expire, cache, cache_model): def __init__(self, func, key, time_expire, cache, cache_model):
self.__name__ = func.__name__ self.__name__ = func.__name__