This repository has been archived by the owner on Sep 29, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathcache.py
214 lines (197 loc) · 9.32 KB
/
cache.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# -*- coding: utf-8 -*-
from server import db, utils, request, tasks, session
from server.config import conf
from hashlib import sha512
from datetime import datetime, timedelta
import logging
from functools import wraps
"""
This module provides a cache, allowing to serve
whole queries from that cache. Unlike other caches
implemented in ViUR, it caches the actual result
(ie the html-output in most cases). This can also
be used to cache the output of custom build functions.
Admins can bypass this cache by sending the X-Viur-Disable-Cache http Header
along with their requests.
"""
viurCacheName = "viur-cache"
def keyFromArgs( f, userSensitive, languageSensitive, evaluatedArgs, path, args, kwargs ):
"""
Parses args and kwargs according to the information's given
by evaluatedArgs and argsOrder. Returns an unique key for this
combination of arguments. This key is guaranteed to be stable for
subsequent calls with the same arguments and context( the current user)
:param f: Callable which is inspected for its signature
(we need to figure out what positional arguments map to which key argument)
:type f: Callable
:param userSensitive: Signals wherever the output of f depends on the current user.
0 means independent of wherever the user is a guest or known, all will get the same content.
1 means cache only for guests, no cache will be performed if the user is logged-in.
2 means cache in two groups, one for guests and one for all users
3 will cache the result of that function for each individual users separately.
:type userSensitive: int
:param evaluatedArgs: List of keyword-arguments having influence to the output generated by
that function. This list *must* complete! Parameters not named here are ignored!
:type evaluatedArgs: list
:param path: Path to the function called but without parameters (ie. "/page/view")
:type path: str
:returns: The unique key derived
"""
res = {}
argsOrder = list( f.__code__.co_varnames )[ 1 : f.__code__.co_argcount ]
# Map default values in
reversedArgsOrder = argsOrder[ : : -1]
for defaultValue in list( f.func_defaults or [] )[ : : -1]:
res[ reversedArgsOrder.pop( 0 ) ] = defaultValue
del reversedArgsOrder
# Map args in
setArgs = [] # Store a list of args already set by *args
for idx in range(0, min( len( args ), len( argsOrder ) ) ):
if argsOrder[ idx ] in evaluatedArgs:
setArgs.append( argsOrder[ idx ] )
res[ argsOrder[ idx ] ] = args[ idx ]
# Last, we map the kwargs in
for k,v in kwargs.items():
if k in evaluatedArgs:
if k in setArgs:
raise AssertionError("Got dupplicate arguments for %s" % k )
res[ k ] = v
if userSensitive:
user = utils.getCurrentUser()
if userSensitive==1 and user: #We dont cache requests for each user seperately
return( None )
elif userSensitive==2:
if user:
res[ "__user" ] = "__ISUSER"
else:
res[ "__user" ] = None
elif userSensitive==3:
if user:
res[ "__user" ] = user["key"]
else:
res[ "__user" ] = None
if languageSensitive:
res[ "__lang" ] = request.current.get().language
if conf[ "viur.cacheEnvironmentKey" ]:
try:
res["_cacheEnvironment"] = conf["viur.cacheEnvironmentKey"]()
except RuntimeError:
return None
res[ "__path" ] = path #Different path might have different output (html,xml,..)
try:
appVersion = request.current.get().request.environ["CURRENT_VERSION_ID"].split('.')[0]
except:
appVersion = ""
logging.error("Could not determine the current application id! Caching might produce unexpected results!")
res["__appVersion"] =appVersion
# Last check, that every parameter is satisfied:
if not all ( [ x in res.keys() for x in argsOrder ] ):
# we have too few paramerts for this function; that wont work
return None
res = list( res.items() ) #Flatn our dict to a list
res.sort( key=lambda x: x[0] ) #sort by keys
mysha512 = sha512()
mysha512.update( unicode(res).encode("UTF8") )
return( mysha512.hexdigest() )
def wrapCallable(f, urls, userSensitive, languageSensitive, evaluatedArgs, maxCacheTime):
"""
Does the actual work of wrapping a callable.
Use the decorator enableCache instead of calling this directly.
"""
@wraps(f)
def wrapF( self, *args, **kwargs ):
currentRequest = request.current.get()
if conf["viur.disableCache"] or currentRequest.disableCache:
# Caching disabled
if conf["viur.disableCache"]:
logging.debug( "Caching is disabled by config" )
return( f( self, *args, **kwargs ) )
# How many arguments are part of the way to the function called (and how many are just *args)
offset = -len( currentRequest.args) or len( currentRequest.pathlist )
path = "/"+"/".join( currentRequest.pathlist[ : offset ] )
if not path in urls:
# This path (possibly a sub-render) should not be cached
logging.debug( "Not caching for %s" % path )
return( f( self, *args, **kwargs ) )
key = keyFromArgs( f, userSensitive, languageSensitive, evaluatedArgs, path, args, kwargs )
if not key:
# Someting is wrong (possibly the parameter-count)
# Letz call f, but we knew already that this will clash
return( f( self, *args, **kwargs ) )
try:
dbRes = db.Get( db.Key.from_path( viurCacheName, key ) )
except db.EntityNotFoundError:
dbRes = None
if dbRes:
if not maxCacheTime or \
dbRes["creationtime"] > datetime.now()-timedelta( seconds=maxCacheTime ):
# We store it unlimited or the cache is fresh enough
logging.debug( "This request was served from cache." )
currentRequest.response.headers['Content-Type'] = dbRes[ "content-type"].encode("UTF-8")
return( dbRes["data"] )
# If we made it this far, the request wasnt cached or too old; we need to rebuild it
res = f( self, *args, **kwargs )
dbEntity = db.Entity( viurCacheName, name=key )
dbEntity[ "data" ] = res
dbEntity[ "creationtime" ] = datetime.now()
dbEntity[ "path" ] = path
dbEntity[ "content-type"] = request.current.get().response.headers['Content-Type']
dbEntity.set_unindexed_properties( ["data","content-type"] ) #We can save 2 DB-Writs :)
db.Put( dbEntity )
logging.debug( "This request was a cache-miss. Cache has been updated." )
return( res )
return wrapF
def enableCache( urls, userSensitive=0, languageSensitive=False, evaluatedArgs=[], maxCacheTime=None ):
"""
Decorator to mark a function cacheable.
Only functions decorated with enableCache are considered cacheable;
ViUR will never ever cache the result of a user-defined function otherwise.
Warning: It's not possible to cache the result of a function relying on reading/modifying
the environment (ie. setting custom http-headers). The only exception is the content-type header.
:param urls: A list of urls for this function, for which the cache should be enabled.
A function can have several urls (eg. /page/view or /pdf/page/view), and it
might should not be cached under all urls (eg. /admin/page/view).
:type urls: list
:param userSensitive: Signals wherever the output of f depends on the current user.
0 means independent of wherever the user is a guest or known, all will get the same content.
1 means cache only for guests, no cache will be performed if the user is logged-in.
2 means cache in two groups, one for guests and one for all users
3 will cache the result of that function for each individual users separately.
:type userSensitive: int
:param languageSensitive: If true, signals that the output of f might got translated.
If true, the result of that function is cached separately for each language.
:type languageSensitive: Bool
:param evaluatedArgs: List of keyword-arguments having influence to the output generated by
that function. This list *must* be complete! Parameters not named here are ignored!
Warning: Double-check this list! F.e. if that function generates a list of entries and
you miss the parameter "order" here, it would be impossible to sort the list.
It would always have the ordering it had when the cache-entry was created.
:type evaluatedArgs: list
:param maxCacheTime: Specifies the maximum time an entry stays in the cache in seconds.
Note: Its not erased from the db after that time, but it won't be served anymore.
If None, the cache stays valid forever (until manually erased by calling flushCache.
:type maxCacheTime: int or None
"""
assert not any( [x.startswith("_") for x in evaluatedArgs]), "A evaluated Parameter cannot start with an underscore!"
return lambda f: wrapCallable( f, urls, userSensitive, languageSensitive, evaluatedArgs, maxCacheTime )
@tasks.callDeferred
def flushCache( prefix="/*" ):
"""
Flushes the cache. Its possible the flush only a part of the cache by specifying
the path-prefix.
:param prefix: Path or prefix that should be flushed.
:type prefix: str
Examples:
- "/" would flush the main page (and only that),
- "/*" everything from the cache, "/page/*" everything from the page-module (default render),
- and "/page/view/*" only that specific subset of the page-module.
"""
items = db.Query( viurCacheName ).filter( "path =", prefix.rstrip("*") ).iter( keysOnly=True )
for item in items:
db.Delete( item )
if prefix.endswith("*"):
items = db.Query( viurCacheName ).filter( "path >", prefix.rstrip("*") ).filter( "path <", prefix.rstrip("*")+u"\ufffd").iter( keysOnly=True )
for item in items:
db.Delete( item )
logging.debug("Flushing cache succeeded. Everything matching \"%s\" is gone." % prefix )
__all__ = [ "enableCache", "flushCache" ]