2014-04-25 13 views
30

Zusammenfassung

Eines unserer Themen in der Produktion einen Fehler getroffen persistierenden und jetzt InvalidRequestError: This session is in 'prepared' state; no further SQL can be emitted within this transaction. Fehler erzeugt, bei jeder Anfrage mit einer Abfrage, die es für den Rest seines Lebens dient! Es ist dies für Tage getan, jetzt! Wie ist das möglich und wie können wir verhindern, dass es weitergeht?Ungültige Transaktion über Anfragen

Hintergrund

Wir sind mit einem Fläschchen App auf uwsgi (4 Prozesse, 2 Themen), mit Flask-SQLAlchemy uns DB-Verbindungen zu SQL Server bereitstellt.

Das Problem schien, als einer unserer Fäden in der Produktion zu beginnen wurde abzureißen seine Forderung, in dieser Flask-SQLAlchemy Methode:

@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 

... und irgendwie geschaffen self.session.commit() zu rufen, wenn die Transaktion ungültig . Dies führte dazu, dass sqlalchemy.exc.InvalidRequestError: Can't reconnect until invalid transaction is rolled back trotz unserer Protokollierungskonfiguration in stdout ausgegeben wurde, was sinnvoll ist, da es während des Herunterfahrens des Anwendungskontextes passiert ist, was niemals Ausnahmen auslösen sollte. Ich bin mir nicht sicher, wie die Transaktion wurde ungültig ohne response_or_exec bekommen, aber das ist eigentlich das kleinere Problem AFAIK.

Das größere Problem ist, dass, wenn die "vorbereiteten" Zustand Fehler begonnen haben, und seitdem nicht aufgehört haben. Jedes Mal, wenn dieser Thread eine Anfrage bedient, die die DB trifft, 500s. Jeder zweite Thread scheint in Ordnung zu sein: Soweit ich das beurteilen kann, macht sogar der Thread, der sich im selben Prozess befindet, OK.

wilde Vermutung

Die SQLAlchemy Mailingliste einen Eintrag über die „‚vorbereiten‘Zustand“ Fehler hat sagen passiert es, wenn eine Sitzung gestartet begehen und hat noch nicht fertig, und etwas anderes versucht, es zu benutzen. Meine Vermutung ist, dass die Sitzung in diesem Thread nie zum self.session.remove() Schritt kam, und jetzt wird es nie.

Ich habe immer noch das Gefühl, dass nicht erklärt, wie diese Sitzung über Anfragen obwohl persistent bleibt. Wir haben Flask-SQLAlchemys Verwendung von Request-Scoped-Sitzungen nicht geändert, daher sollte die Sitzung an den SQLAlchemy-Pool zurückgegeben und am Ende der Anfrage zurückgerollt werden, selbst wenn sie fehlerhaft sind (obwohl zugegebenermaßen wahrscheinlich nicht die erste ist), seit dem während des app-kontextes abreißen). Warum passieren die Rollbacks nicht? Ich könnte es verstehen, wenn wir jedes Mal die "ungültige Transaktion" -Fehler auf stdout (in uwsgis Logbuch) sehen würden, aber wir sind es nicht: Ich habe es nur einmal gesehen, das erste Mal. Aber ich sehe den "vorbereiteten Status" Fehler (in unserem App-Log) jedes Mal, wenn die 500s auftreten.

Konfigurationsdetails

Wir haben expire_on_commit im session_options ausgeschaltet, und wir haben auf SQLALCHEMY_COMMIT_ON_TEARDOWN gedreht. Wir lesen nur aus der Datenbank, schreiben noch nicht. Wir verwenden auch Dogpile-Cache für alle unsere Abfragen (mit der Memcached-Sperre, da wir mehrere Prozesse haben und eigentlich zwei Server mit Lastenausgleich). Der Cache läuft jede Minute für unsere Hauptanfrage ab.

Aktualisiert 2014.04.28: Auflösung Schritte

Neustart der Server das Problem behoben, die nicht ganz überraschend zu haben scheint. Das heißt, ich erwarte, es wieder zu sehen, bis wir herausfinden, wie man es stoppt.benselme (unten) schlug vor, unseren eigenen Teardown-Callback mit Exception-Handling um das Commit zu schreiben, aber ich glaube, das größere Problem ist, dass der Thread für den Rest seines Lebens durcheinander gebracht wurde. Die Tatsache, dass diese nicht weggehen nach einer Anfrage oder zwei wirklich macht mich nervös!

+0

Der Thread wurde durcheinander gebracht ** weil ** die 'Session' nicht' remove'd war. Ich persönlich würde Flask-SQLAlchemy ablehnen: Dieser Bug wird zum Beispiel schwer zu umgehen sein, und wenn Sie sich den github Repo ansehen, werden Sie sehen, dass er nicht mehr wirklich gepflegt wird. Außerdem gibt es nicht viel mehr, als was SQLAlchemy bereits tut. – benselme

+0

@benselme: Eine wichtige Sache, die Flask-SQLAlchemy * bietet, sind Request-Scoped-Sitzungen.Wenn eine neue Anforderung kommt, sollte eine neue Sitzung generiert werden, unabhängig davon, ob die alte Sitzung entfernt wurde oder nicht. Das ist ein Teil des Grundes, warum das so seltsam ist: Wenn irgend etwas hätte, hätte eine DB-Verbindung zu lange geöffnet bleiben müssen, kein Thread in einem permanenten Fehlerzustand. Ich stimme zu, es ist komisch, wie ruhig Flask-SQLAlchemy ist, aber es ist * vom Flask-Autor geschrieben, also denke ich, dass es ziemlich stabil ist. –

+0

@benselme, ich habe das gleiche über Flask-SQLAlchemy gefühlt. Und ich habe meinen eigenen "SQLAlchemy to Flask" Integrationscode geschrieben. Und schließlich sah ich mich mit dem gleichen seltsamen Verhalten konfrontiert, wie es in dieser Frage beschrieben wurde. –

Antwort

25

bearbeiten 2016.06.05:

Ein PR, die dieses Problem am 26. Mai 2016.

Flask PR 1822

bearbeiten 2015.04.13 verschmolzen wurde, löst:

Rätsel gelöst!

TL; DR: Seien Sie absolut sicher Ihre Teardown Funktionen erfolgreich zu sein, indem die Teardown-Wrapping Rezept in der 2014.12.11 bearbeiten verwenden!

Begann einen neuen Job auch mit Flask, und dieses Problem tauchte wieder auf, bevor ich das Teardown-Wrapping-Rezept einrichtete. Also habe ich dieses Problem noch einmal durchdacht und herausgefunden, was passiert ist.

Wie ich dachte, schiebt Flask einen neuen Anforderungskontext auf den Anforderungskontextstapel jedes Mal, wenn eine neue Anfrage auf der Linie kommt. Dies wird verwendet, um globale Request-Locals wie die Sitzung zu unterstützen.

Flask hat auch eine Vorstellung von "Anwendung" -Kontext, der vom Anfragekontext getrennt ist. Es soll Dinge wie Testen und CLI-Zugriff unterstützen, wo HTTP nicht stattfindet. Ich wusste das, und ich wusste auch, dass Flask-SQLA dort seine DB-Sitzungen ablegt.

Während des normalen Betriebs werden sowohl ein Anfrage- als auch ein App-Kontext am Anfang einer Anfrage und am Ende gedrückt.

jedoch stellt sich heraus, dass, wenn ein Anforderungskontext drängen, überprüft der Anforderungskontext, ob eine vorhandene App Zusammenhang gibt es, und wenn man die Gegenwart, es ein neues Geschäft nicht Push tut!

Also, wenn der App Kontext ist nicht am Ende einer Anfrage tauchte aufgrund einer Erhöhung Teardown-Funktion, es wird nicht nur für immer bleiben, um, wird es nicht einmal ein neuer App Kontext hat geschoben oben davon.

Das erklärt auch etwas Magie, die ich in unseren Integrationstests nicht verstanden hatte. Sie können einige Testdaten EINFÜGEN und dann einige Anfragen ausführen, und diese Anfragen können auf diese Daten zugreifen, obwohl Sie nicht zugesagt haben. Dies ist nur möglich, da die Anforderung einen neuen Anforderungskontext hat, den Kontext der Testanwendung jedoch wiederverwendet, sodass die vorhandene DB-Verbindung wiederverwendet wird. Das ist also wirklich ein Feature, kein Bug.

Das heißt, es bedeutet, Sie müssen absolut sicher sein, dass Ihre Teardown-Funktionen erfolgreich sind, mit etwas wie der Teardown-Funktion Wrapper unten. Das ist auch ohne dieses Feature eine gute Idee, um Speicher- und DB-Verbindungen zu vermeiden, ist aber angesichts dieser Ergebnisse besonders wichtig. Aus diesem Grund werde ich Flasks Unterlagen einreichen. (Here it is)

bearbeitet 2014.12.11:

Eine Sache, die wir an Ort und Stelle landeten Putting war der folgende Code (in unserer Anwendung Fabrik), die jede Teardown-Funktion wickelt, um sicherzustellen, dass es die protokolliert Ausnahme und erhebt nicht weiter. Dadurch wird sichergestellt, dass der App-Kontext immer erfolgreich abgerufen wird. Offensichtlich muss dies gehen nach Sie sind sicher, dass alle Teardown-Funktionen registriert wurden.

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

bearbeiten 2014.09.19:

Ok, stellt sich heraus, --reload-on-exception ist keine gute Idee, wenn 1.) Sie mehrere Threads und 2) Beenden eines Threads Mitte verwenden Anfrage könnte Probleme verursachen. Ich dachte, uWSGI würde warten, bis alle Anfragen für diesen Worker beendet sind, wie uWSGIs "graceful reload" -Feature, aber das scheint nicht der Fall zu sein. Wir hatten Probleme damit, dass ein Thread in Memcached eine Dogpile-Sperre erlangte und dann beendet wurde, wenn uWSGI den Worker aufgrund einer Ausnahme in einem anderen Thread neu lud, was bedeutet, dass die Sperre niemals freigegeben wird.

Entfernen SQLALCHEMY_COMMIT_ON_TEARDOWN gelöst Teil unseres Problems, obwohl wir immer noch gelegentliche Fehler bei der App Teardown währendsession.remove() bekommen. Es scheint, dass diese von SQLAlchemy issue 3043 verursacht werden, die in Version 0.9.5 behoben wurde, so dass hoffentlich ein Upgrade auf 0.9.5 uns erlauben wird, auf den App-Kontext teardown immer zu arbeiten.

Original:

Wie dies in erster Linie passiert ist, ist noch eine offene Frage, aber ich habe einen Weg finden, um es zu verhindern: --reload-on-exception Wahl des uwsgi.

Die Fehlerbehandlung unserer Flask-App sollte fast alles erfassen, daher kann es eine benutzerdefinierte Fehlerreaktion liefern, was bedeutet, dass nur die meisten unerwarteten Ausnahmen es bis zu uWSGI machen sollten. Es macht also Sinn, die gesamte App neu zu laden, wenn das passiert.

Wir werden auch SQLALCHEMY_COMMIT_ON_TEARDOWN ausschalten, obwohl wir wahrscheinlich explizit committen, anstatt unseren eigenen Rückruf für App-Teardown zu schreiben, da wir so selten in die Datenbank schreiben.

+0

Das ist wirklich richtige Antwort und Arbeitslösung. Getestet auf unsere Produktion. –

+0

Ich hatte ein ähnliches Problem auf einer Website mit wenig Verkehr und es stellt sich heraus, dass der DB-Provider MySQL-Verbindungen um 60 Sekunden getötet, um "Run-Away-Prozesse" zu steuern. Standardmäßig recycelt SQL Alchemy keine Verbindungen, um mit diesem Verhalten zu arbeiten. Ein Teil des Problems besteht darin, dass Flask den Kontext auf viele verschiedene und subtile Weisen verwaltet und der Teil, in dem das Aufräumen/Recyceln stattfinden sollte, wurde nicht korrekt überprüft. Die Lösung für meine Anwendung war '' 'app.config ['SQLALCHEMY_POOL_RECYCLE'] = 45''' in meiner Konfiguration hinzuzufügen, um eine Verbindung vor dem Provider zu schließen. Standard war '' 'None'''. Hoffe das hilft anderen! –

4

Eine überraschende Sache ist, dass es keine Ausnahmebehandlung um das self.session.commit gibt. Ein Commit kann zum Beispiel fehlschlagen, wenn die Verbindung zur Datenbank unterbrochen wird. Das Commit schlägt fehl, session wird nicht entfernt, und das nächste Mal, wenn der bestimmte Thread eine Anforderung verarbeitet, versucht er immer noch, diese jetzt ungültige Sitzung zu verwenden.

Leider bietet Flask-SQLAlchemy keine saubere Möglichkeit, eine eigene Teardown-Funktion zu haben. Eine Möglichkeit wäre, die SQLALCHEMY_COMMIT_ON_TEARDOWN auf False gesetzt zu haben und dann Ihre eigene Teardown-Funktion zu schreiben.

Es sollte wie folgt aussehen:

@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 

Nun werden Sie noch Ihren andernfalls verpflichtet, und Sie werden feststellen, dass separat untersuchen müssen ... Aber zumindest sollte der Thread erholen.

+1

Ich dachte auch in diese Richtung. Zumindest würden wir mehr Kontrolle bekommen. Einige Vorschläge: Wenn 'SQLALCHEMY_COMMIT_ON_TEARDOWN' False ist, entfernt der Rückruf von Flask-SQLA immer die Sitzung, also sollten wir es nicht machen. Ich würde auch einen 'ausgenommen SQLAlchemyError:' Block schreiben, der nur die Tracebacks protokolliert anstatt zu erhöhen, da ich nicht glaube, dass Flask irgendwelche Anstrengungen unternimmt, um Ausnahmen zu behandeln, die während dieser Rückrufe vorkommen. –

Verwandte Themen