Possible bug in redis_cache.py with multiple apps served by different domains

34 views
Skip to first unread message

Lisandro

unread,
May 21, 2015, 7:51:45 AM5/21/15
to web...@googlegroups.com
I thought I should show the case here, before submitting any issue, because I'm not sure if it's really a bug.

I'm using web2py with multiple apps, each app served by a specific domain. For this, I'm using parameter-based router, and I've set exclusive_domain=True (with this, the app name is removed from the url). All the apps have been caching in RAM some returned htmls, like this:

@cache.action(time_expire=30, cache_model=cache.ram, session=False, vars=False, public=True)
def index():
    ...
    return response.render()


Up to here, all good. Now, if I change to Redis, all the apps start seeing **the same cache**, and this is what happens: I hit the domain of the first app, the returned HTML is cached. If I hit any of the other domains within the next 30 seconds (time_expire=30), the returned HTML is the one cached by the first hit to the first app. It's like cache.redis isn't distinguishing between applications.


I was able to "solve the problem" with any of the following:
  * Adding a prefix to @cache.action, somethink like prefix=request.application (not a real solution, because it would be necessary to modify all the code where cache.action is used)
  * Explicitly including the appname in the url (neither a real solution, the idea is to remove the appname from url and serve it from an specific domain)


I looked into the file web2py/gluon/contrib/redis_cache.py, and I see this from the line 92 (inside the __init__ method of the RedisClient class):
        self.prefix = "w2p:%s:" % (self.request.application)
        if self.request:
            app = self.request.application
        else:
            app = ''

I also checked cache.redis.stats(), and I noticed this portion of the stats:
w2p_keys:
w2p:demo:___cache_set:23870125_expire_in_sec:38L
w2p:demo:___cache_set:23870126_expire_in_sec:98L
w2p:demo:___cache_set:23870127_expire_in_sec:158L
w2p:demo:___cache_set_expire_in_sec:None
w2p:demo:f03addd369f2c5d060faf9cfae3e5c61_expire_in_sec:19L


On thing I noticed is that the key of the form "w2p:appname" shows **always** the name of the first installed app (excluding admin app). Either specifying the appname in the url or not, the key is always generated using the name of the first installed app. Even if I do a flushall in redis and hit for the first time the application X, when I check those stats, I see always that the key was generated using the name of the first installed app.

Is this the expected behaviour? 

Niphlod

unread,
May 21, 2015, 12:15:04 PM5/21/15
to web...@googlegroups.com
the prefix is dependant from request.application, so as long as that one changes, I don't see how it'll be merged with another application. The keys you have seems to point out that "demo" is the application name....

Lisandro

unread,
May 21, 2015, 1:13:10 PM5/21/15
to web...@googlegroups.com
Let me post a more detailed example.

This is a test with three applications.

Two of them ("demo" and "fundapres") use redis to cache the html for default/index, with this decorator (notice empty prefix):
@cache.action(time_expire=30, cache_model=cache.redis, session=False, vars=False, public=True, prefix='')

The third doesn't directly use redis to cache, but instantiates it to show cache.redis.stats(). Acordingly to documentation, the stats() method "returns a dictionary with statistics of Redis server with one additional key ('w2p_keys') showing all keys currently set from web2py with their TTL"
If I don't misunderstand that, w2p_keys shows all keys currently set from web2py for all the installed apps that are using redis.

The first test. Hitting these two urls:
/demo/default/index
/fundapres/default/index

Everything works ok. When I check cache.redis.stats (from the third application) I see this
w2p_keys:
w2p:webmedios:075da5fa9bae34693cee9778e8b893c2_expire_in_sec:29L
w2p:webmedios:___cache_set:23870442_expire_in_sec:166L
w2p:webmedios:___cache_set_expire_in_sec:None
w2p:webmedios:ab19050d1dfd233a5191fddbad56a4ec_expire_in_sec:26L

Notice the first line and the last one. Those are the stored keys, one for /demo/default/index, and the other for /fundapres/default/index.


However, in production, I don't want to access applications with the appname in the url, so I map domains to apps.
So, with everything working ok as shown before, now I hit the following the same two urls, but accessed in this way:

Here w2p_keys has this:
w2p_keys:
w2p:webmedios:4c750e2df1c6be696b0d0a310fe1a6e2_expire_in_sec:27L
w2p:webmedios:___cache_set:23870452_expire_in_sec:150L
w2p:webmedios:___cache_set_expire_in_sec:None

Notice that one only one key is created (the first line), corresponding to default/index of the first hit. In this case, first hit was to demo app (throgh demo.dev domain). 
The second hit (to fundapres app throught fundapres.dev domain), returns the same html cached for demo app.


I can "solve" this problem modifying, adding a custom prefix to the @cache.action used by each application:
# for demo app
@cache.action(
time_expire=30, cache_model=cache.redis, session=False, vars=False, public=True, prefix='demo')  

# for fundapres app
@cache.action(time_expire=30, cache_model=cache.redis, session=False, vars=False, public=True, prefix='fundapres')


In this case, I can access default/index of both application through the corresponding domain, without having to specify appname in the url. That way it does work, and this is w2p_keys:
w2p_keys:
w2p:webmedios:___cache_set:23870420_expire_in_sec:170L
w2p:webmedios:___cache_set_expire_in_sec:None
w2p:webmedios:demo4c750e2df1c6be696b0d0a310fe1a6e2_expire_in_sec:24L
w2p:webmedios:fundapres4c750e2df1c6be696b0d0a310fe1a6e2_expire_in_sec:27L

As expected, the prefix specified in @cache.action is added to the key, after the "w2p:xxxxxx:"
But I thought the keys would be like w2p:demo:...., w2p:fundapres:....

I've tryied to understand why the "w2p:webmedios:" is always the begining of the key, regardless the request.application is demo or fundapres.
I've noticed that the "w2p:xxxxx" is generated in the "first run", using the current app. I did a flushall in redis, restarted nginx and uwsgi, and then the "w2p:xxxxx" is generated with the appname of the first run.

Maybe I'm not using redis cache correctly. But I followed the documentation, very simple. 
from gluon.contrib.redis_cache import RedisCache
cache
.redis = RedisCache('localhost:6379', db=None, debug=True, with_lock=True)

Again, the problem goes away if I add a prefix in @cache.action, but I think that prefix is optional and has other purpose. 

Niphlod

unread,
May 21, 2015, 2:44:21 PM5/21/15
to web...@googlegroups.com


On Thursday, May 21, 2015 at 7:13:10 PM UTC+2, Lisandro wrote:
Let me post a more detailed example.

This is a test with three applications.

Two of them ("demo" and "fundapres") use redis to cache the html for default/index, with this decorator (notice empty prefix):
@cache.action(time_expire=30, cache_model=cache.redis, session=False, vars=False, public=True, prefix='')

The third doesn't directly use redis to cache, but instantiates it to show cache.redis.stats(). Acordingly to documentation, the stats() method "returns a dictionary with statistics of Redis server with one additional key ('w2p_keys') showing all keys currently set from web2py with their TTL"
If I don't misunderstand that, w2p_keys shows all keys currently set from web2py for all the installed apps that are using redis.

nope. only the one pertaining to the application. https://github.com/web2py/web2py/blob/master/gluon/contrib/redis_cache.py#L265

that being said, I **think** that the issue may be indeed that the RedisCache instance is istantiated at the first access. This never happened to me (heavy redis user here :-P) because I have only one app per redis host.

I'd argue that a fix **can** be (https://github.com/web2py/web2py/blob/master/gluon/contrib/redis_cache.py#L68 and following)

    locker
.acquire()
   
try:
        instance_name
= 'redis_instance_' + current.request.application
       
if not hasattr(RedisCache, instance_name):
            setattr
(RedisCache, instance_name, RedisClient(*args, **vars))
       
return getattr(RedisCache, instance_name)
   
finally:
        locker
.release()



let me know if it fixes the problem.

Lisandro

unread,
May 21, 2015, 3:25:12 PM5/21/15
to web...@googlegroups.com
Awesome! The fix worked like a charm ;)

Thank you very much Niphlod, I really appreciate your help, you're definitely a wise "web2py guru". 
I've been always grateful for the help received here. I really want to give something back, but then I read the questions posted, and I really can't find one that I could answer with precision :P

Anyway, thanks again. Let me know if I have to submit a ticket or something like that.

Niphlod

unread,
May 21, 2015, 4:27:03 PM5/21/15
to web...@googlegroups.com
Reply all
Reply to author
Forward
0 new messages