better cache-disk, thanks Leonel
This commit is contained in:
@@ -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 @@
|
|||||||
Version 2.9.6-stable+timestamp.2014.09.04.22.16.03
|
Version 2.9.6-stable+timestamp.2014.09.04.22.27.48
|
||||||
|
|||||||
+126
-99
@@ -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):
|
||||||
try:
|
"""
|
||||||
if self.storage:
|
Implements a key based storage in disk.
|
||||||
self.storage.close()
|
"""
|
||||||
except ValueError:
|
def __init__(self, folder):
|
||||||
pass
|
self.folder = folder
|
||||||
finally:
|
# Check the best way to do atomic file replacement.
|
||||||
self.storage = None
|
if sys.version_info >= (3, 3):
|
||||||
if self.locker and self.locked:
|
self.replace = os.replace
|
||||||
portalocker.unlock(self.locker)
|
elif sys.platform == "win32":
|
||||||
self.locker.close()
|
import ctypes
|
||||||
self.locked = False
|
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 _open_shelve_and_lock(self):
|
def replace_windows(src, dst):
|
||||||
"""Open and return a shelf object, obtaining an exclusive lock
|
if not ReplaceFile(dst, src, None, 0, 0, 0):
|
||||||
on self.locker first. Replaces the close method of the
|
os.rename(src, dst)
|
||||||
returned shelf instance with one that releases the lock upon
|
|
||||||
closing."""
|
|
||||||
|
|
||||||
storage = None
|
self.replace = replace_windows
|
||||||
locker = None
|
else:
|
||||||
locked = False
|
# POSIX rename() is always atomic
|
||||||
try:
|
self.replace = os.rename
|
||||||
locker = locker = open(self.locker_name, 'a')
|
|
||||||
portalocker.lock(locker, portalocker.LOCK_EX)
|
|
||||||
locked = True
|
def __setitem__(self, key, value):
|
||||||
|
tmp_name, tmp_path = tempfile.mkstemp(dir=self.folder)
|
||||||
|
tmp = os.fdopen(tmp_name, 'wb')
|
||||||
try:
|
try:
|
||||||
storage = shelve.open(self.shelve_name)
|
pickle.dump((time.time(), value), tmp, pickle.HIGHEST_PROTOCOL)
|
||||||
except:
|
finally:
|
||||||
logger.error('corrupted cache file %s, will try rebuild it'
|
tmp.close()
|
||||||
% self.shelve_name)
|
fullfilename = os.path.join(self.folder, recfile.generate(key))
|
||||||
storage = None
|
if not os.path.exists(os.path.dirname(fullfilename)):
|
||||||
if storage is None:
|
os.makedirs(os.path.dirname(fullfilename))
|
||||||
if os.path.exists(self.shelve_name):
|
self.replace(tmp_path, fullfilename)
|
||||||
os.unlink(self.shelve_name)
|
|
||||||
storage = shelve.open(self.shelve_name)
|
|
||||||
if not CacheAbstract.cache_stats_name in storage.keys():
|
def __getitem__(self, key):
|
||||||
storage[CacheAbstract.cache_stats_name] = {
|
if recfile.exists(key, path=self.folder):
|
||||||
'hit_total': 0, 'misses': 0}
|
timestamp, value = pickle.load(recfile.open(key, 'rb', path=self.folder))
|
||||||
storage.sync()
|
return value
|
||||||
except Exception, e:
|
else:
|
||||||
if storage:
|
raise KeyError
|
||||||
storage.close()
|
|
||||||
storage = None
|
def __contains__(self, key):
|
||||||
if locked:
|
return recfile.exists(key, path=self.folder)
|
||||||
portalocker.unlock(locker)
|
|
||||||
locker.close()
|
|
||||||
locked = False
|
def __delitem__(self, key):
|
||||||
raise RuntimeError(
|
recfile.remove(key, path=self.folder)
|
||||||
'unable to create/re-create cache file %s' % self.shelve_name)
|
|
||||||
self.locker = locker
|
|
||||||
self.locked = locked
|
def __iter__(self):
|
||||||
self.storage = storage
|
for dirpath, dirnames, filenames in os.walk(self.folder):
|
||||||
return storage
|
for filename in filenames:
|
||||||
|
yield filename
|
||||||
|
|
||||||
|
|
||||||
|
def get(self, key, default=None):
|
||||||
|
try:
|
||||||
|
return self[key]
|
||||||
|
except KeyError:
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
for key in self:
|
||||||
|
del self[key]
|
||||||
|
|
||||||
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 self.storage[key]
|
||||||
del storage[key]
|
|
||||||
storage.sync()
|
if f is None:
|
||||||
now = time.time()
|
return None
|
||||||
if f is None:
|
|
||||||
value = None
|
now = time.time()
|
||||||
elif item and (dt is None or item[0] > now - dt):
|
|
||||||
value = item[1]
|
if item and ((dt is None) or (item[0] > now - dt)):
|
||||||
else:
|
value = item[1]
|
||||||
value = f()
|
else:
|
||||||
storage[key] = (now, value)
|
value = f()
|
||||||
storage[CacheAbstract.cache_stats_name]['misses'] += 1
|
self.storage[key] = (now, value)
|
||||||
storage.sync()
|
self.storage[CacheAbstract.cache_stats_name]['misses'] += 1
|
||||||
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__
|
||||||
|
|||||||
Reference in New Issue
Block a user