Подтвердить что ты не робот

Неверная транзакция, сохраняющаяся во всех запросах

Резюме

Один из наших потоков в производстве попал в ошибку и теперь производит ошибки InvalidRequestError: This session is in 'prepared' state; no further SQL can be emitted within this transaction. по каждому запросу с запросом, который он обслуживает, на всю оставшуюся жизнь! Это делалось уже несколько дней! Как это возможно, и как мы можем предотвратить его продвижение?

Фон

Мы используем приложение Flask для uWSGI (4 процесса, 2 потока), а Flask-SQLAlchemy предоставляет нам подключения DB к SQL Server.

Проблема, казалось, начиналась, когда один из наших потоков в производстве срывал свой запрос внутри метода Flask-SQLAlchemy:

@teardown
def shutdown_session(response_or_exc):
    if app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN']:
        if response_or_exc is None:
            self.session.commit()
    self.session.remove()
    return response_or_exc

... и каким-то образом удалось вызвать self.session.commit(), когда транзакция была недействительной. Это привело к тому, что sqlalchemy.exc.InvalidRequestError: Can't reconnect until invalid transaction is rolled back получал вывод в stdout, что противоречит нашей конфигурации ведения журнала, что имеет смысл, поскольку это произошло во время разрыва приложения, что никогда не должно приводить к исключениям. Я не уверен, как транзакция оказалась недействительной без response_or_exec получения набора, но это на самом деле меньшая проблема AFAIK.

Большая проблема заключается в том, что когда началось "подготовленное состояние", ошибки и не прекратились. Каждый раз, когда этот поток обслуживает запрос, который попадает в БД, он 500s. Кажется, что каждый другой поток выглядит точным: насколько я могу судить, даже поток, который в том же процессе работает нормально.

Дикая догадка

В списке рассылки SQLAlchemy есть запись об ошибке "подготовленное состояние", в котором говорится, что это происходит, если сеанс начался и еще не закончен, а что-то еще пытается его использовать. Я предполагаю, что сеанс в этом потоке никогда не попадал на шаг self.session.remove(), и теперь он никогда не будет.

Я все еще чувствую, что это не объясняет, как этот сеанс сохраняется через запросы. Мы не модифицировали использование флажков-SQLAlchemy сеансов с запросом, поэтому сеанс должен быть возвращен в пул SQLAlchemy и откат в конце запроса, даже те, которые являются ошибками (хотя, по общему признанию, вероятно, не первый, так как это увеличилось во время разговора приложения). Почему откаты не происходят? Я мог бы понять это, если бы каждый раз видели ошибки "недействительной транзакции" на stdout (в журнале uwsgi), но мы не: я видел это только один раз, в первый раз. Но я вижу ошибку "подготовленного состояния" (в нашем журнале приложений) каждый раз, когда происходят 500.

Сведения о конфигурации

Мы отключили expire_on_commit в session_options, и мы включили SQLALCHEMY_COMMIT_ON_TEARDOWN. Мы только читаем из базы данных, а не пишем. Мы также используем Dogpile-Cache для всех наших запросов (с использованием блокировки memcached, так как у нас есть несколько процессов и, фактически, 2 сервера с балансировкой нагрузки). Кэш заканчивается каждую минуту для нашего основного запроса.

Обновлено 2014-04-28: Шаги разрешения

Перезапуск сервера, похоже, устранил проблему, что не удивительно. Тем не менее, я ожидаю увидеть это снова, пока мы не выясним, как остановить его. benselme (ниже) предложил написать наш собственный обратный вызов с разумом с обработкой исключений вокруг фиксации, но я чувствую, что большая проблема заключается в том, что поток был испорчен до конца своей жизни. Тот факт, что это не исчезло после того, как запрос или два действительно заставляет меня нервничать!

4b9b3361

Ответ 1

Изменить 2016-06-05:

PR, который решает эту проблему, был объединен 26 мая 2016 года.

Flask PR 1822

Изменить 2015-04-13:

Тайна решена!

TL; DR: Будьте абсолютно уверены, что ваши функции прерывания будут успешными, используя рецепт обертывания в редакции 2014-12-11!

Начиналось новое задание, также использующее Flask, и эта проблема возникла снова, прежде чем я применил рецепт обертывания. Поэтому я пересмотрел этот вопрос и, наконец, выяснил, что произошло.

Как я и думал, Flask подталкивает новый контекст запроса в стек контекста запроса каждый раз, когда новый запрос идет по строке. Это используется для поддержки локальных запросов-локальных, например сеанса.

В колбе также есть понятие контекста "приложения", которое отделено от контекста запроса. Он предназначен для поддержки таких функций, как тестирование и доступ к CLI, где HTTP не происходит. Я знал это, и я также знал, что там, где Flask-SQLA ставит свои сеансы БД.

Во время нормальной работы как запрос, так и контекст приложения помещаются в начале запроса и выталкиваются в конце.

Однако оказывается, что при нажатии контекста запроса контекст запроса проверяет, существует ли существующий контекст приложения, и если он присутствует, он не нажимает новый!

Итак, если контекст приложения не выставляется в конце запроса из-за повышения функции разрыва, он не только будет оставаться навсегда, но даже не будет иметь контекста приложения, расположенного поверх него.

Это также объясняет некоторые магии, которые я не понял в наших интеграционных тестах. Вы можете ВСТАВИТЬ некоторые тестовые данные, затем выполнить некоторые запросы, и эти запросы смогут получить доступ к этим данным, несмотря на то, что вы не совершаете. Это возможно только после того, как запрос имеет новый контекст запроса, но повторно использует контекст тестового приложения, поэтому он повторно использует существующее соединение с БД. Так что это действительно особенность, а не ошибка.

Тем не менее, это означает, что вам нужно быть абсолютно уверенными в том, что ваши функции разрыва преуспевают, используя что-то вроде обертки функции teardown. Это хорошая идея, даже без этой функции, чтобы избежать утечки памяти и соединений БД, но особенно важно в свете этих находок. По этой причине я отправлю документы PR для Flask. (Вот он)

Изменить 2014-12-11:

В результате мы создали следующий код (в нашем приложении factory), который обертывает каждую функцию teardown, чтобы убедиться, что он регистрирует исключение и не увеличивает его. Это гарантирует, что контекст приложения всегда успешно всплывает. Очевидно, это должно произойти после того, как вы убедитесь, что все функции разрывов зарегистрированы.

# 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)

Изменить 2014-09-19:

Хорошо, получается --reload-on-exception не очень хорошая идея, если: 1.) вы используете несколько потоков и 2.) прекращение запроса на поток может вызвать проблемы. Я думал, что uWSGI будет ждать завершения всех запросов для этого работника, например, функция uWSGI "изящная перезагрузка", но, похоже, это не так. У нас возникли проблемы с тем, что нить приобретет блокировку Dople в Memcached, а затем прекратится, когда uWSGI перезагрузит рабочего из-за исключения в другом потоке, то есть блокировка никогда не будет выпущена.

Удаление SQLALCHEMY_COMMIT_ON_TEARDOWN решило часть нашей проблемы, хотя мы по-прежнему получаем случайные ошибки во время отключения приложения во время session.remove(). По-видимому, это вызвано проблемой SQLAlchemy 3043, которая была исправлена ​​в версии 0.9.5, поэтому, надеюсь, обновление до 0.9.5 позволит нам полагаться при работе с контекстом приложения всегда работает.

Оригинал:

Как это произошло в первую очередь, это открытый вопрос, но я нашел способ его предотвратить: uWSGI --reload-on-exception.

Ошибка обработки ошибок в приложении Flask должна быть улавлива-лась почти так, поэтому она может обслуживать пользовательский ответ об ошибке, а это означает, что только самые неожиданные исключения должны сделать все возможное для uWSGI. Поэтому имеет смысл перезагрузить все приложение, когда это произойдет.

Мы также отключим SQLALCHEMY_COMMIT_ON_TEARDOWN, хотя мы, скорее всего, будем передавать явно, а не писать собственный обратный вызов для приложения teardown, так как мы редко записываем в базу данных.

Ответ 2

Удивительно, что с этим self.session.commit нет обработки исключений. И фиксация может завершиться неудачей, например, если соединение с БД будет потеряно. Таким образом, коммит не выполняется, session не удаляется, и в следующий раз, когда конкретный поток обрабатывает запрос, он по-прежнему пытается использовать этот недействительный сеанс.

К сожалению, Flask-SQLAlchemy не предлагает никакой чистой возможности иметь свою собственную функцию разрыва. Один из способов состоял бы в том, чтобы установить SQLALCHEMY_COMMIT_ON_TEARDOWN в значение False, а затем записать свою собственную функцию разрыва.

Он должен выглядеть так:

@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

Теперь у вас все еще будут свои неудачные коммиты, и вам придется исследовать это отдельно... Но по крайней мере ваш поток должен восстановиться.