Invalid transaction persisting across requests

后端 未结 2 775
盖世英雄少女心
盖世英雄少女心 2020-12-23 20:17

Summary

One of our threads in production hit an error and is now producing InvalidRequestError: This session is in \'prepared\' state; no further SQL can be

相关标签:
2条回答
  • 2020-12-23 20:34

    Edit 2016-06-05:

    A PR that solves this problem has been merged on May 26, 2016.

    Flask PR 1822

    Edit 2015-04-13:

    Mystery solved!

    TL;DR: Be absolutely sure your teardown functions succeed, by using the teardown-wrapping recipe in the 2014-12-11 edit!

    Started a new job also using Flask, and this issue popped up again, before I'd put in place the teardown-wrapping recipe. So I revisited this issue and finally figured out what happened.

    As I thought, Flask pushes a new request context onto the request context stack every time a new request comes down the line. This is used to support request-local globals, like the session.

    Flask also has a notion of "application" context which is separate from request context. It's meant to support things like testing and CLI access, where HTTP isn't happening. I knew this, and I also knew that that's where Flask-SQLA puts its DB sessions.

    During normal operation, both a request and an app context are pushed at the beginning of a request, and popped at the end.

    However, it turns out that when pushing a request context, the request context checks whether there's an existing app context, and if one's present, it doesn't push a new one!

    So if the app context isn't popped at the end of a request due to a teardown function raising, not only will it stick around forever, it won't even have a new app context pushed on top of it.

    That also explains some magic I hadn't understood in our integration tests. You can INSERT some test data, then run some requests and those requests will be able to access that data despite you not committing. That's only possible since the request has a new request context, but is reusing the test application context, so it's reusing the existing DB connection. So this really is a feature, not a bug.

    That said, it does mean you have to be absolutely sure your teardown functions succeed, using something like the teardown-function wrapper below. That's a good idea even without that feature to avoid leaking memory and DB connections, but is especially important in light of these findings. I'll be submitting a PR to Flask's docs for this reason. (Here it is)

    Edit 2014-12-11:

    One thing we ended up putting in place was the following code (in our application factory), which wraps every teardown function to make sure it logs the exception and doesn't raise further. This ensures the app context always gets popped successfully. Obviously this has to go after you're sure all teardown functions have been registered.

    # Flask specifies that teardown functions should not raise.
    # However, they might not have their own error handling,
    # so we wrap them here to log any errors and prevent errors from
    # propagating.
    def wrap_teardown_func(teardown_func):
        @wraps(teardown_func)
        def log_teardown_error(*args, **kwargs):
            try:
                teardown_func(*args, **kwargs)
            except Exception as exc:
                app.logger.exception(exc)
        return log_teardown_error
    
    if app.teardown_request_funcs:
        for bp, func_list in app.teardown_request_funcs.items():
            for i, func in enumerate(func_list):
                app.teardown_request_funcs[bp][i] = wrap_teardown_func(func)
    if app.teardown_appcontext_funcs:
        for i, func in enumerate(app.teardown_appcontext_funcs):
            app.teardown_appcontext_funcs[i] = wrap_teardown_func(func)
    

    Edit 2014-09-19:

    Ok, turns out --reload-on-exception isn't a good idea if 1.) you're using multiple threads and 2.) terminating a thread mid-request could cause trouble. I thought uWSGI would wait for all requests for that worker to finish, like uWSGI's "graceful reload" feature does, but it seems that's not the case. We started having problems where a thread would acquire a dogpile lock in Memcached, then get terminated when uWSGI reloads the worker due to an exception in a different thread, meaning the lock is never released.

    Removing SQLALCHEMY_COMMIT_ON_TEARDOWN solved part of our problem, though we're still getting occasional errors during app teardown during session.remove(). It seems these are caused by SQLAlchemy issue 3043, which was fixed in version 0.9.5, so hopefully upgrading to 0.9.5 will allow us to rely on the app context teardown always working.

    Original:

    How this happened in the first place is still an open question, but I did find a way to prevent it: uWSGI's --reload-on-exception option.

    Our Flask app's error handling ought to be catching just about anything, so it can serve a custom error response, which means only the most unexpected exceptions should make it all the way to uWSGI. So it makes sense to reload the whole app whenever that happens.

    We'll also turn off SQLALCHEMY_COMMIT_ON_TEARDOWN, though we'll probably commit explicitly rather than writing our own callback for app teardown, since we're writing to the database so rarely.

    0 讨论(0)
  • 2020-12-23 20:43

    A surprising thing is that there's no exception handling around that self.session.commit. And a commit can fail, for example if the connection to the DB is lost. So the commit fails, session is not removed and next time that particular thread handles a request it still tries to use that now-invalid session.

    Unfortunately, Flask-SQLAlchemy doesn't offer any clean possibility to have your own teardown function. One way would be to have the SQLALCHEMY_COMMIT_ON_TEARDOWN set to False and then writing your own teardown function.

    It should look like this:

    @app.teardown_appcontext
    def shutdown_session(response_or_exc):
        try: 
            if response_or_exc is None:
                sqla.session.commit()
        finally:
            sqla.session.remove()
        return response_or_exc
    

    Now, you will still have your failing commits, and you'll have to investigate that separately... But at least your thread should recover.

    0 讨论(0)
提交回复
热议问题