I have an object that is used to store some parameters of a global application. These settings can be edited using the administrator’s HTML page, but rarely change. I have only one instance of this object (single type) and always refer to this instance when I need access to the settings.
Here it comes down to:
class Settings(ndb.Model): SINGLETON_DATASTORE_KEY = 'SINGLETON' @classmethod def singleton(cls): return cls.get_or_insert(cls.SINGLETON_DATASTORE_KEY) foo = ndb.IntegerProperty( default = 100, verbose_name = "Some setting called 'foo'", indexed = False) @ndb.tasklet def foo():
I was on the assumption that calling Settings.singleton() more than once for the request handler will be quite fast, since the first call will most likely extract the Settings object from memcache (since the object is rarely updated) and all subsequent calls within the same request handler will be retrieve it from the cache in NDB context mode. From the doc:
The cache in the context is stored only for one incoming HTTP request and is "visible" only to the code that processes this request. It is fast; This cache lives in memory.
However, AppStat shows that my Settings object is retrieved from memcache several times within the same request handler. I know this by looking at the detailed request handler page in AppStat, expanding the call trace of each call to memcache.Get and looking at the memcahe key that is being renamed.
In request handlers, I use a lot of tablets, and I call Settings.singleton() from tasks that need access to the settings. Could this be the reason that the settings object is retrieved from memcache again, and not from the cache in context? If so, what are the exact rules that determine when / when an object can be retrieved from the cache in context or not? I could not find this information in the NDB documentation.
Update 2013/02/15: I cannot reproduce this in a dummy test application. Test code:
class Foo(ndb.Model): prop_a = ndb.DateTimeProperty(auto_now_add = True) def use_foo(): foo = Foo.get_or_insert('singleton') logging.info("Function using foo: %r", foo.prop_a) @ndb.tasklet def use_foo_tasklet(): foo = Foo.get_or_insert('singleton') logging.info("Function using foo: %r", foo.prop_a) @ndb.tasklet def use_foo_async_tasklet(): foo = yield Foo.get_or_insert_async('singleton') logging.info("Function using foo: %r", foo.prop_a) class FuncGetOrInsertHandler(webapp2.RequestHandler): def get(self): for i in xrange(10): logging.info("Iteration %d", i) use_foo() class TaskletGetOrInsertHandler(webapp2.RequestHandler): @ndb.toplevel def get(self): logging.info("Toplevel") use_foo() for i in xrange(10): logging.info("Iteration %d", i) use_foo_tasklet() class AsyncTaskletGetOrInsertHandler(webapp2.RequestHandler): @ndb.toplevel def get(self): logging.info("Toplevel") use_foo() for i in xrange(10): logging.info("Iteration %d", i) use_foo_async_tasklet()
Before starting any of the validation handlers, make sure that there is a Foo object with the key name singleton.
Unlike what I see in my working application, all of these request handlers show a single memcache.Get call in Appstats.
Update 2013/02/21:. Finally, I can reproduce this in a dummy test application. Test code:
class ToplevelAsyncTaskletGetOrInsertHandler(webapp2.RequestHandler): @ndb.toplevel def get(self): logging.info("Toplevel 1") use_foo() self._toplevel2() @ndb.toplevel def _toplevel2(self): logging.info("Toplevel 2") use_foo() for i in xrange(10): logging.info("Iteration %d", i) use_foo_async_tasklet()
This handler shows 2 memcache.Get calls in Appstats, like my production code.
In fact, in my production request handler encoding, I have a toplevel called by another toplevel . toplevel create a new ndb context.
Changing the nested toplevel to synctasklet fixes the problem.