update remove queries one delete datatypes python flask sqlalchemy uwsgi flask-sqlalchemy

python - remove - queries sqlalchemy



Transacción no válida que persiste en las solicitudes (2)

Resumen

Uno de nuestros subprocesos en producción llegó a un error y ahora está produciendo InvalidRequestError: This session is in ''prepared'' state; no further SQL can be emitted within this transaction. InvalidRequestError: This session is in ''prepared'' state; no further SQL can be emitted within this transaction. errores, en cada solicitud con una consulta que sirve, ¡por el resto de su vida! ¡Ha estado haciendo esto por días , ahora! ¿Cómo es esto posible y cómo podemos prevenirlo?

Fondo

Estamos utilizando una aplicación Flask en uWSGI (4 procesos, 2 subprocesos), con Flask-SQLAlchemy que nos proporciona conexiones de BD a SQL Server.

El problema parecía comenzar cuando uno de nuestros hilos en producción estaba derribando su solicitud, dentro de este método 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

... y de alguna manera logró llamar a self.session.commit() cuando la transacción no era válida. Esto dio como resultado sqlalchemy.exc.InvalidRequestError: Can''t reconnect until invalid transaction is rolled back obteniendo salida a stdout, desafiando nuestra configuración de registro, lo cual tiene sentido ya que ocurrió durante el contexto de la aplicación derribando, lo que nunca se supone excepciones No estoy seguro de cómo la transacción llegó a ser inválida sin response_or_exec establecerse, pero ese es en realidad el problema menor AFAIK.

El problema más grande es que es cuando comenzaron los errores del ''estado preparado'', y no se han detenido desde entonces. Cada vez que este hilo cumple una solicitud que llega al DB, es 500s. Cada otro hilo parece estar bien: por lo que puedo decir, incluso el hilo que está en el mismo proceso está haciendo bien.

Conjetura salvaje

La lista de correo SQLAlchemy tiene una entrada sobre el error "estado preparado" que dice que sucede si una sesión comenzó a comprometerse y aún no ha terminado, y otra cosa intenta usarla. Supongo que la sesión en este hilo nunca llegó al paso self.session.remove() , y ahora nunca lo hará.

Todavía siento que eso no explica cómo esta sesión persiste en todas las solicitudes . No hemos modificado el uso de Flask-SQLAlchemy de las sesiones con ámbito de solicitud, por lo que la sesión debería devolverse al grupo de SQLAlchemy y retrotraerse al final de la solicitud, incluso las que están cometiendo errores (aunque es cierto que probablemente no sea la primera). ya que surgió durante el contexto de la aplicación derribando). ¿Por qué no están sucediendo los retrocesos? Podría entenderlo si estuviéramos viendo los errores de "transacción no válida" en stdout (en el registro de uwsgi) cada vez, pero no estamos: solo lo vi una vez, la primera vez. Pero veo el error de "estado preparado" (en el registro de nuestra aplicación) cada vez que ocurre el 500s.

Detalles de configuración

Hemos desactivado expire_on_commit en session_options , y hemos activado SQLALCHEMY_COMMIT_ON_TEARDOWN . Solo estamos leyendo desde la base de datos, no escribiendo todavía. También usamos Dogpile-Cache para todas nuestras consultas (utilizando el bloqueo de memcached ya que tenemos múltiples procesos, y en realidad, 2 servidores de equilibrio de carga). La caché caduca cada minuto para nuestra consulta principal.

Actualizado el 28/04/2014: pasos de resolución

Reiniciar el servidor parece haber solucionado el problema, lo que no es del todo sorprendente. Dicho eso, espero volver a verlo hasta que descubramos cómo detenerlo. benselme (abajo) sugirió que escribiéramos nuestra propia devolución de llamada demorada con manejo de excepción alrededor del compromiso, pero siento que el problema más grande es que el hilo fue arruinado por el resto de su vida. El hecho de que esto no desapareció después de una o dos solicitudes realmente me pone nervioso.


Una cosa sorprendente es que no hay ninguna excepción en el manejo de ese self.session.commit . Y una confirmación puede fallar, por ejemplo si se pierde la conexión a la base de datos. Por lo tanto, la confirmación falla, la session no se elimina y la próxima vez que esa cadena maneje una solicitud, intenta usar esa sesión que ahora no es válida.

Desafortunadamente, Flask-SQLAlchemy no ofrece ninguna posibilidad clara de tener su propia función de desmontaje. Una forma sería establecer SQLALCHEMY_COMMIT_ON_TEARDOWN en False y luego escribir su propia función de desmontaje.

Debe tener un aspecto como este:

@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

Ahora, aún tendrá sus commits fallidos, y tendrá que investigar eso por separado ... Pero al menos su hilo debería recuperarse.


Editar 2016-06-05:

Un RP que resuelve este problema se fusionó el 26 de mayo de 2016.

Frasco PR 1822

Editar 2015-04-13:

¡Misterio resuelto!

TL; DR: ¡Esté absolutamente seguro de que sus funciones de desmontaje tienen éxito, usando la receta de envoltura en la edición 2014-12-11!

Empecé un nuevo trabajo también usando Flask, y este problema apareció nuevamente, antes de poner en práctica la receta de envoltura. Así que revisé este tema y finalmente descubrí qué sucedió.

Como pensé, Flask empuja un nuevo contexto de solicitud en la pila de contexto de solicitud cada vez que una nueva solicitud llega a la línea. Esto se usa para admitir globales globales de solicitud, como la sesión.

Flask también tiene una noción de contexto de "aplicación" que está separada del contexto de solicitud. Está destinado a admitir cosas como las pruebas y el acceso CLI, donde HTTP no está sucediendo. Sabía esto, y también sabía que allí es donde Flask-SQLA pone sus sesiones de DB.

Durante el funcionamiento normal, tanto una solicitud como un contexto de aplicación se presionan al comienzo de una solicitud y se muestran al final.

Sin embargo, al presionar un contexto de solicitud, el contexto de solicitud comprueba si hay un contexto de aplicación existente, y si hay uno presente, ¡ no empuja uno nuevo!

Por lo tanto, si el contexto de la aplicación no aparece al final de una solicitud debido a un aumento de la función de desmontaje, no solo se mantendrá para siempre, sino que ni siquiera tendrá un nuevo contexto de aplicación sobre el mismo.

Eso también explica algo de magia que no había entendido en nuestras pruebas de integración. Puedes INSERTAR algunos datos de prueba, luego ejecutar algunas solicitudes y esas solicitudes podrán acceder a esos datos a pesar de que no te comprometas. Esto solo es posible porque la solicitud tiene un nuevo contexto de solicitud, pero está reutilizando el contexto de la aplicación de prueba, por lo que reutiliza la conexión de base de datos existente. Entonces esto realmente es una característica, no un error.

Dicho eso, significa que tienes que estar absolutamente seguro de que tus funciones de desmontaje tienen éxito, usando algo como el envoltorio de la función de desmontaje a continuación. Es una buena idea, incluso sin esa característica, para evitar pérdidas de memoria y conexiones de DB, pero es especialmente importante a la luz de estos hallazgos. Presentaré un PR a los documentos de Flask por este motivo. ( Aquí está )

Editar 2014-12-11:

Una cosa que terminamos implementando fue el siguiente código (en nuestra fábrica de aplicaciones), que ajusta cada función de extracción para asegurarse de que registra la excepción y no sube más. Esto garantiza que el contexto de la aplicación siempre aparezca correctamente. Obviamente, esto tiene que pasar después de que esté seguro de que se han registrado todas las funciones de desmontaje.

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

Editar 2014-09-19:

Ok, resulta que --reload-on-exception no es una buena idea si 1.) estás usando varios hilos y 2.) terminar un hilo a mitad de la solicitud podría causar problemas. Pensé que UTSGI esperaría a que finalizaran todas las solicitudes de ese trabajador, como lo hace la función "recarga agraciada" de uWSGI, pero parece que ese no es el caso. Empezamos a tener problemas en los que un hilo adquiriría un bloqueo de perno en Memcached, luego se daría por terminado cuando uWSGI recargue al trabajador debido a una excepción en un hilo diferente, lo que significa que el bloqueo nunca se liberará.

La eliminación de SQLALCHEMY_COMMIT_ON_TEARDOWN resolvió parte de nuestro problema, aunque todavía estamos recibiendo errores ocasionales durante el cierre de la aplicación durante session.remove() . Parece que estos son causados ​​por SQLAlchemy issue 3043 , que se corrigió en la versión 0.9.5, por lo que con suerte la actualización a 0.9.5 nos permitirá confiar en que el desmontaje del contexto de la aplicación siempre funcionará.

Original:

Cómo ha sucedido esto en primer lugar sigue siendo una pregunta abierta, pero encontré una forma de prevenirlo: uWSGI - opción de --reload-on-exception .

El manejo de errores de nuestra aplicación Flask debería atrapar casi cualquier cosa, por lo que puede servir para una respuesta de error personalizada, lo que significa que solo las excepciones más inesperadas deberían llegar hasta uWSGI. Por lo tanto, tiene sentido volver a cargar toda la aplicación cada vez que eso ocurra.

También desactivaremos SQLALCHEMY_COMMIT_ON_TEARDOWN , aunque probablemente nos comprometamos explícitamente en lugar de escribir nuestra propia devolución de llamada para el cierre de la aplicación, ya que estamos escribiendo en la base de datos muy raramente.