unfortunately, I don't have a reproducible test case for this behaviour; it's a "heisenbug" that only hits sometimes. Running my single unit tests from terminal against the SDK I experience both 10 failures in a row and 30 successes in a row before a failure; sometimes it can be really difficult to trigger and I almost forget the problem before it hits again. The non-determinism seems to be related to the NDB async event loop -- is there a way to hack NDB to make the event loop less random, or failing that, more random? (Waiting and randomizing the event queue or something?)
The problem is a BadRequestError("Cannot start a new operation in a finished transaction."). Seems like what happens is that a call to ndb.non_transactional shuts down the calling transaction. However, the code of the transaction continues running as if nothing happened, leading to the BadRequestError.
The situation is described in the following snippet. Note that when such a reduced test-case is run by itself I never get the error, but I have a setup now with the same snippet except for things going on before the error, as indicated.
# a lot of things here that "should" be unrelated but is needed for bug to hit; may well
# be doing something "wrong"...
print "after call to inner" # this is printed AFTER stack trace pasted below
(use_cache and use_memcache doesn't seem to be relevant).
For some diagnostics, I patched context.py, line 946 (in a "finally" clause inside Context.transaction, where the transaction shuts down), to add traceback.print_stack(). When the failure hits, the stack trace attached appears. It appears BEFORE the "after call to inner" line, so the transaction is indeed shut down by the non_transactional call, but the txn function continues running.
Anything I'm doing wrong, or any advice in how to reduce this to a more deterministic test-case? Any red flags I should look for in the code leading up to this?
File "/home/dagss/myproject/myproject/bin/nosetests", line 57, in <module>
sys.exit(nose.run_exit())
File "/home/dagss/myproject/myproject/eggs/nose-1.3.0-py2.7.egg/nose/core.py", line 118, in __init__
**extra_args)
File "/home/dagss/myproject/local/lib/python2.7/unittest/main.py", line 95, in __init__
self.runTests()
File "/home/dagss/myproject/myproject/eggs/nose-1.3.0-py2.7.egg/nose/core.py", line 197, in runTests
result = self.testRunner.run(self.test)
File "/home/dagss/myproject/myproject/eggs/nose-1.3.0-py2.7.egg/nose/core.py", line 61, in run
test(result)
File "/home/dagss/myproject/myproject/eggs/nose-1.3.0-py2.7.egg/nose/suite.py", line 176, in __call__
return self.run(*arg, **kw)
File "/home/dagss/myproject/myproject/eggs/nose-1.3.0-py2.7.egg/nose/suite.py", line 223, in run
test(orig)
File "/home/dagss/myproject/myproject/eggs/nose-1.3.0-py2.7.egg/nose/suite.py", line 176, in __call__
return self.run(*arg, **kw)
File "/home/dagss/myproject/myproject/eggs/nose-1.3.0-py2.7.egg/nose/suite.py", line 223, in run
test(orig)
File "/home/dagss/myproject/myproject/eggs/nose-1.3.0-py2.7.egg/nose/suite.py", line 176, in __call__
return self.run(*arg, **kw)
File "/home/dagss/myproject/myproject/eggs/nose-1.3.0-py2.7.egg/nose/suite.py", line 223, in run
test(orig)
File "/home/dagss/myproject/myproject/eggs/nose-1.3.0-py2.7.egg/nose/case.py", line 45, in __call__
return self.run(*arg, **kwarg)
File "/home/dagss/myproject/myproject/eggs/nose-1.3.0-py2.7.egg/nose/case.py", line 133, in run
self.runTest(result)
File "/home/dagss/myproject/myproject/eggs/nose-1.3.0-py2.7.egg/nose/case.py", line 151, in runTest
test(result)
File "/home/dagss/myproject/local/lib/python2.7/unittest/case.py", line 391, in __call__
return self.run(*args, **kwds)
File "/home/dagss/myproject/local/lib/python2.7/unittest/case.py", line 327, in run
testMethod()
File "/home/dagss/myproject/myproject/src/myproject/myproject/core/tests/test_something.py", line 64, in test_happy_day
status=201)
File "/home/dagss/myproject/myproject/src/myproject/myproject/core/testsupport/base_api_tester.py", line 70, in put
return self.call('PUT', url, data, **kw)
File "/home/dagss/myproject/myproject/src/myproject/myproject/core/testsupport/base_api_tester.py", line 49, in call
headers=headers)
File "/home/dagss/myproject/myproject/eggs/WebTest-2.0-py2.7.egg/webtest/app.py", line 255, in put
content_type=content_type,
File "/home/dagss/myproject/myproject/eggs/WebTest-2.0-py2.7.egg/webtest/app.py", line 594, in _gen_request
expect_errors=expect_errors)
File "/home/dagss/myproject/myproject/eggs/WebTest-2.0-py2.7.egg/webtest/app.py", line 467, in do_request
res = req.get_response(app, catch_exc_info=True)
File "/home/dagss/myproject/myproject/parts/google_appengine/lib/webob-1.2.3/webob/request.py", line 1292, in send
application, catch_exc_info=True)
File "/home/dagss/myproject/myproject/parts/google_appengine/lib/webob-1.2.3/webob/request.py", line 1260, in call_application
app_iter = application(self.environ, start_response)
File "/home/dagss/myproject/myproject/eggs/WebTest-2.0-py2.7.egg/webtest/lint.py", line 194, in lint_app
iterator = application(environ, start_response_wrapper)
File "/home/dagss/myproject/myproject/src/myproject/myproject/auth/middleware.py", line 33, in __call__
return self.application(environ, start_response)
File "/home/dagss/myproject/myproject/parts/google_appengine/lib/webapp2-2.5.2/webapp2.py", line 1529, in __call__
rv = self.router.dispatch(request, response)
File "/home/dagss/myproject/myproject/parts/google_appengine/lib/webapp2-2.5.2/webapp2.py", line 1278, in default_dispatcher
return route.handler_adapter(request, response)
File "/home/dagss/myproject/myproject/parts/google_appengine/lib/webapp2-2.5.2/webapp2.py", line 1102, in __call__
return handler.dispatch()
File "/home/dagss/myproject/myproject/parts/google_appengine/google/appengine/ext/ndb/tasklets.py", line 1049, in add_context_wrapper
return synctaskletfunc(*args, **kwds)
File "/home/dagss/myproject/myproject/parts/google_appengine/google/appengine/ext/ndb/tasklets.py", line 1030, in synctasklet_wrapper
return taskletfunc(*args, **kwds).get_result()
File "/home/dagss/myproject/myproject/parts/google_appengine/google/appengine/ext/ndb/tasklets.py", line 325, in get_result
self.check_success()
File "/home/dagss/myproject/myproject/parts/google_appengine/google/appengine/ext/ndb/tasklets.py", line 320, in check_success
self.wait()
File "/home/dagss/myproject/myproject/parts/google_appengine/google/appengine/ext/ndb/tasklets.py", line 304, in wait
if not ev.run1():
File "/home/dagss/myproject/myproject/parts/google_appengine/google/appengine/ext/ndb/eventloop.py", line 219, in run1
delay = self.run0()
File "/home/dagss/myproject/myproject/parts/google_appengine/google/appengine/ext/ndb/eventloop.py", line 181, in run0
callback(*args, **kwds)
File "/home/dagss/myproject/myproject/parts/google_appengine/google/appengine/ext/ndb/tasklets.py", line 474, in _on_future_completion
self._help_tasklet_along(ns, ds_conn, gen, val)
File "/home/dagss/myproject/myproject/parts/google_appengine/google/appengine/ext/ndb/tasklets.py", line 371, in _help_tasklet_along
value = gen.send(val)
File "/home/dagss/myproject/myproject/src/myproject/myproject/handlers/base/core.py", line 220, in dispatch
result = method(*args, **kwargs)
File "/home/dagss/myproject/myproject/src/myproject/myproject/auth/authinfo.py", line 141, in new_f
return f(*args, **kwargs)
File "/home/dagss/myproject/myproject/src/myproject/myproject/core/apibase/decorators.py", line 72, in new_func
result = result.get_result()
File "/home/dagss/myproject/myproject/parts/google_appengine/google/appengine/ext/ndb/tasklets.py", line 325, in get_result
self.check_success()
File "/home/dagss/myproject/myproject/parts/google_appengine/google/appengine/ext/ndb/tasklets.py", line 320, in check_success
self.wait()
File "/home/dagss/myproject/myproject/parts/google_appengine/google/appengine/ext/ndb/tasklets.py", line 304, in wait
if not ev.run1():
File "/home/dagss/myproject/myproject/parts/google_appengine/google/appengine/ext/ndb/eventloop.py", line 219, in run1
delay = self.run0()
File "/home/dagss/myproject/myproject/parts/google_appengine/google/appengine/ext/ndb/eventloop.py", line 181, in run0
callback(*args, **kwds)
File "/home/dagss/myproject/myproject/parts/google_appengine/google/appengine/ext/ndb/tasklets.py", line 371, in _help_tasklet_along
value = gen.send(val)
File "/home/dagss/myproject/myproject/src/myproject/myproject/handlers/somehandler.py", line 77, in put
r = ndb.non_transactional(f)()
File "/home/dagss/myproject/myproject/parts/google_appengine/google/appengine/ext/ndb/utils.py", line 173, in inner_wrapper
return wrapped_decorator(func, args, kwds, **options)
File "/home/dagss/myproject/myproject/parts/google_appengine/google/appengine/ext/ndb/model.py", line 3670, in non_transactional
return func(*args, **kwds)
File "/home/dagss/myproject/myproject/src/myproject/myproject/handlers/somehandler.py", line 69, in f
key.get(use_cache=False, use_memcache=False)
File "/home/dagss/myproject/myproject/parts/google_appengine/google/appengine/ext/ndb/key.py", line 532, in get
return self.get_async(**ctx_options).get_result()
File "/home/dagss/myproject/myproject/parts/google_appengine/google/appengine/ext/ndb/tasklets.py", line 325, in get_result
self.check_success()
File "/home/dagss/myproject/myproject/parts/google_appengine/google/appengine/ext/ndb/tasklets.py", line 320, in check_success
self.wait()
File "/home/dagss/myproject/myproject/parts/google_appengine/google/appengine/ext/ndb/tasklets.py", line 304, in wait
if not ev.run1():
File "/home/dagss/myproject/myproject/parts/google_appengine/google/appengine/ext/ndb/eventloop.py", line 219, in run1
delay = self.run0()
File "/home/dagss/myproject/myproject/parts/google_appengine/google/appengine/ext/ndb/eventloop.py", line 181, in run0
callback(*args, **kwds)
File "/home/dagss/myproject/myproject/parts/google_appengine/google/appengine/ext/ndb/tasklets.py", line 474, in _on_future_completion
self._help_tasklet_along(ns, ds_conn, gen, val)
File "/home/dagss/myproject/myproject/parts/google_appengine/google/appengine/ext/ndb/tasklets.py", line 371, in _help_tasklet_along
value = gen.send(val)
File "/home/dagss/myproject/myproject/parts/google_appengine/google/appengine/ext/ndb/context.py", line 947, in transaction
traceback.print_stack()