[Bottle] Clean-Up nach Broken Pipe

Django, Flask, Bottle, WSGI, CGI…
Antworten
glocke
User
Beiträge: 66
Registriert: Mittwoch 23. Februar 2011, 21:18

Hi,

ich suche eine Möglichkeit - nach einer Broken Pipe (nehmen wir mal an durch das Abbrechen eines Seite-Ladens) - noch etwas zu tun - z.B. was aufzuräumen. Hintergrund ist, dass (in meinem Browsergame-Projekt) eine DB-Abfrage (Elixir bzw. SQL-Alchemy in Verbindung mit MySQL) bei einer UPDATE-Transaktion das jeweilige Feld vorher sperrt und nach Beendigung des Schreibens wieder entsperrt. Blöderweise passiert es immermal, dass - wenn das Laden der Seite / des Ajax-Requests abgebrochen wird - die Transaktion nicht komplett ist und das jeweilige Feld gesperrt bleibt. Ich habe im Datenbank-Unterforum das Problem bereits formuliert (http://www.python-forum.de/viewtopic.php?f=23&t=29933), allerdings denke ich, dass es eher an bottle liegt (zumindest die Sache mit dem "etwas nach einer Broken Pipe tun").

Oder vereinfacht ausgedrückt folgender Code:

Code: Alles auswählen

from bottle import route, run
from threading import Lock

field_lock = Lock() # sei mal der Lock auf dem zu verwendenden Datenfeld

# Dekorator, um automatisch die Session zu commiten oder zurückzurollen
def managed_transaction(func):
    def _managed_transaction(*args, **kw):
        try:
            res = func(*args, **kw)
            print 'Commit!'
        except Exception as e:
            print 'Rollback!'
        return res
    return _managed_transaction

# eine "fehlerfreie" Transaktion
@route('/complete/')
@managed_transaction
def complete():
    print 'Waiting'
    with field_lock:
        print 'Locked!'
        print 'Accessed' # das wäre der eigentliche Schreib-Zugriff
    print 'Unlocked!'

# eine sehr langwierige Transaktion (warum auch immer sie in der Praxis so lange dauern mag)
@route('/incomplete/')
@managed_transaction
def incomplete():
    print 'Waiting'
    with field_lock:
        print 'Locked!'
        while True: # das Schreiben verzögert sich (warum auch immer) sehr lange - hier mal extrem als Endlosschleife
            pass
        print 'Accessed'
    print 'Unlocked!'

run()
Führe ich das Skript nun aus und gehe auf 'localhost:8080/complete/' erscheint in der Konsole:

Code: Alles auswählen

Waiting
Locked!
Accessed
Unlocked!
Commit!
Gehe ich dann auf die 'incomplete'-Route erscheint nur ein

Code: Alles auswählen

Waiting
Locked!
Nun muss ich sie im Browser abbrechen (ist ja ne Endlosschleife). Genau das entspricht dem Original-Szenario. Im Code müsste (da will ich hin) nun eine "Behandlung" der Broken Pipe kommen (um die Session zurückzurollen und den Lock aufzuheben). Allerdings finde ich da keine Möglichkeit im bottle-Framework. Führe ich die 'complete'-Route aus, bleibt er nun ebenfalls stehen (der Lock ist ja gesetzt!)

Ich hoffe, ich habe es möglichst verständlich beschrieben :)

LG Glocke
deets

Eine Moeglichkeit, die ich sehe: der Transaktionsdekorator, den ich dir vorgeschlagen haben, hat eine Schwaeche: er funktioniert ausschliesslich um die Methode herum.

Wenn du aber zB das Bottle Templating System benutzt, und DB-Objekte zurueckgibst in deiner Action, die dann *nach* dem Dekorator das Template befuellen - dann ist der Session-Zustand irgendwie undefiniert und koennte solche Probleme verursachen.

Versuch doch mal, den Transaktionsdekorator um einen Middleware herum zu stricken, die du dann um die bottle-WSGI-app legst. Und diese gewrappte Middleware (du koenntest zB dafuer repoze.tm2 benutzen, das integriert sich mit einer Extension mit SA/Elixir) ist dann das, was du vom bottle-server oder deinem WSGI-Container ausliefern laesst.

Die broken Pipes sollten dann eigentlich nicht mehr wirklich ein Problem darstellen - ausser du machst was mit generatoren, aber dann wird's kompliziert.
glocke
User
Beiträge: 66
Registriert: Mittwoch 23. Februar 2011, 21:18

Hi,

ich verwende die SimpleTemplate Engine von Bottle.
deets hat geschrieben:Wenn du aber zB das Bottle Templating System benutzt, und DB-Objekte zurueckgibst in deiner Action, die dann *nach* dem Dekorator das Template befuellen - dann ist der Session-Zustand irgendwie undefiniert und koennte solche Probleme verursachen.
Du meinst, wenn ich den view-Dekorator verwende? Da stimme ich dir zu. Allerdings würde das Problem mit dem undefinierten Session-Zustand wegfallen, wenn ich direkt am Ende (aber noch innerhalb) der transaktions-dekorierten Funktion das Template erzeuge und fülle - an der Stelle, wo der Transaktionsdekorator noch greift. Dann sollte die Session einen gültigen Zustand haben und Fehler abgefangen werden, oder?

Ob ich nun

Code: Alles auswählen

@route('/bla/foo/route')
@view('bla/foo/template')
def bla_foo():
    doing_foo()
    return dict(foo=data)
oder

Code: Alles auswählen

@route('/bla/foo/route')
def bla_foo():
    doing_foo()
    return template('bla/foo/template', foo=data)
schreibe, macht imho kaum einen Unterschied. Somit wäre die Funktion nur noch mit route dekoriert und ich kann meinen Transaktionsdekorator dazusetzen.

Ich sträube mich irgendwie davor, noch mehr Bibliotheken (z.B. die von dir vorgeschlagene Middleware) einzubinden, um das Problem zu lösen. Naja, ich bilde mir ein, dass das Problem auch ohne Einsatz zusätzlicher Middleware lösbar sein sollte :P

LG Glocke
deets

Wenn das so einfach geht, dann versuch's mit den Templates mal so. Aber sei dir *sicher*, dass da nicht ein generator zurueckgegeben wird. Ist unwahrscheinlich, aber better safe than sorry.

Und eine Middleware ist zb das hier:

Code: Alles auswählen

def wrapper(downstream_app):
      def app(environ, start_response):
            print "ick bin eene middlewaahre, wa"
            return downstream_app(environ, start_response)
     return app
Nicht gerade Raketenwissenschaft fuer die Problemloesung.. ;)
glocke
User
Beiträge: 66
Registriert: Mittwoch 23. Februar 2011, 21:18

deets hat geschrieben:

Code: Alles auswählen

def wrapper(downstream_app):
      def app(environ, start_response):
            print "ick bin eene middlewaahre, wa"
            return downstream_app(environ, start_response)
     return app
Nicht gerade Raketenwissenschaft fuer die Problemloesung.. ;)
:mrgreen:

Btw hab ich den Transaktionsdekorator, die Änderung bzgl. des Templatings implementiert und eine Kleinigkeit im "tiefen" Sourcecode des Spiels geändert (totale Dummheit die wahrscheinlich für die Verzögerung beim Laden und somit das entstehen des Lock-Wait-Timeouts gesorgt hat - boah das tat weh als ich die Stelle gesehen hab ^^ ). Jetzt kann ich den Fehler zumindest nicht mehr reproduzieren. Mal schauen *rumprobier*

LG Glocke

/EDIT: hier mal mein Transaktions-ExceptionLogging-Dekorator:

Code: Alles auswählen

def managed_transaction(func):
    def _managed_transaction(*args, **kw):
        try:
            res = func(*args, **kw)
            session.commit()
        except Exception as e:
            if isinstance(e, HTTPResponse):
                session.commit()
                raise
            session.rollback()
            syslog.log(e)
            redirect(u'/internal_error/')
        return res
    return _managed_transaction
Hinweis: Im Falle einer HTTPResponse-"Exception" (verwendet von bottle für redirect() ) sollte ich besser auch die session commiten, bevor ich den Response raise (und die Umleitung fortführe). syslog ist nen selbstgeschriebener Logger - werde ich später durch das logging-Modul ersetzen.
deets

Auch da wieder - ne Middleware fuer das Exception-Handling waere besser. Du mischst jetzt Transaktions-Handling mit spezifischem Error-handling.

Besser, diese beiden Aspekte zu trennen. WSGI erlaubt genau das.
glocke
User
Beiträge: 66
Registriert: Mittwoch 23. Februar 2011, 21:18

deets hat geschrieben:Auch da wieder - ne Middleware fuer das Exception-Handling waere besser. Du mischst jetzt Transaktions-Handling mit spezifischem Error-handling.

Besser, diese beiden Aspekte zu trennen. WSGI erlaubt genau das.
Naja aber ich muss die Fehler eh einmal abfangen um den Rollback zu machen. Meinst du ich soll den reraisen und dann von der Middleware (die ich um die bottle app lege, right?) das eigentliche Abfangen und loggen machen lassen?
deets

Ja, lieber re-raisen. Du kannst dann zB sowas wie weberror um die App legen, welches dir gleich ein debugger in den browser rendert, oder fuer Produktionsbetrieb mails verschickt und so.
glocke
User
Beiträge: 66
Registriert: Mittwoch 23. Februar 2011, 21:18

Naja gut aber wo ist der Vorteil, wenn ich das nicht vom Dekorator mit machen lasse?

Code: Alles auswählen

from bottle import app, get, run, HTTPResponse

def exception_wrapper(app):
    def _exception_wrapper(envir, start_response):
        try:
            return app(envir, start_response)
        except HTTPResponse:
            raise
        except Exception as e:
            print 'logged :{0}'.format(e)
    return _exception_wrapper

def managed_transaction(func):
    def _managed_transaction(*args, **kw):
        try:
            res = func(*args, **kw)
            print 'session.commit'
            return res
        except HTTPResponse:
            print 'session.commit also at redirect'
            raise
        except Exception as e:
            print 'session.rollback'
            raise
    return _managed_transaction

app = app()

@app.route('/')
@managed_transaction
def index():
    raise ValueError('Foo bar')

# apply middleware
app = exception_wrapper(app)

run(app, host=u'127.0.0.1', port=8080)
Würde das dann so ablaufen? Klappt nämlich nicht :?

LG Glocke
Zuletzt geändert von glocke am Donnerstag 30. August 2012, 10:34, insgesamt 1-mal geändert.
deets

Dir fehlt ein

Code: Alles auswählen

 app = app()
 app.catchall = False
Und dann kommt's natuerlich zu Folgefehlern, weil dein Exception-Handling keine propere WSGI-App ist.
glocke
User
Beiträge: 66
Registriert: Mittwoch 23. Februar 2011, 21:18

Okay das bringt mich schonmal weiter. Dann bekomme ich allerdings

Code: Alles auswählen

Traceback (most recent call last):
  File "/usr/lib/python2.7/wsgiref/handlers.py", line 86, in run
    self.finish_response()
  File "/usr/lib/python2.7/wsgiref/handlers.py", line 126, in finish_response
    for data in self.result:
TypeError: 'NoneType' object is not iterable
weil ich im exception_wrapper (nach dem Loggen) nix zurückgebe. Ohne Fehler wird die gecallte app zurückgegeben. Was geb ich im Fehlerfall stattdessen zurück?

Naja dann stellt sich mir immernoch die Frage: Warum Wrapper UND Dekorator :?:

LG Glocke :)
deets

Du musst natuerlich das WSGI-Protokoll sprechen. Also start_response mit einem redirect-header aufrufen, und ein leeres Iterable zurueckgeben zB.

Alllerdings gebe ich zu, dass ist vielleicht alles etwas viel des guten ist. Bzw. vielleicht nahelegt, ein Framework wie Django zu benutzen, wo an sowas alles schon gedacht ist. Oder eben weberror benutzen.

Alternativ koenntest du deine Awendung anders aufbauen, und die Transaktions-Ebene *unterhalb* der Actions machen, indem du deinen Code, der die eigentliche Anwendung darstellst, kapselst + mit Transaktionsmanagement ausruestest.

Und warum die Trennug? Ganz einfach: es sind zwei verschiedene Dinge, und sie zu vermengen fuehrt jetzt schon zu ziemlichem Spaghetti. Und Transaktionsmanagement ist kritisch, das will man klar trennen. Und wenn du zB Hintergrundprozesse hast, die auch die DB bearbeiten - dann hast du da zwar ne Transaktion und profitierst von dem Dekorator, aber kein HTTP involviert.
glocke
User
Beiträge: 66
Registriert: Mittwoch 23. Februar 2011, 21:18

deets hat geschrieben:Alllerdings gebe ich zu, dass ist vielleicht alles etwas viel des guten ist.
Deswegen frag ich nach dem Sinn der Trennung. 8)
deets hat geschrieben:Und warum die Trennug? Ganz einfach: es sind zwei verschiedene Dinge, und sie zu vermengen fuehrt jetzt schon zu ziemlichem Spaghetti. Und Transaktionsmanagement ist kritisch, das will man klar trennen. Und wenn du zB Hintergrundprozesse hast, die auch die DB bearbeiten - dann hast du da zwar ne Transaktion und profitierst von dem Dekorator, aber kein HTTP involviert.
Naja einer der Hintergrundprozesse verwaltet die Kampfberechnungen. Dabei hab' ich die calculate-Methode in einen Try-Except-Block gesetzt. Dort werden die Fehler abgefangen, geloggt und die Transaktion zurückgerollt - oder committet, wenn kein Fehler auftrat.

Von daher würde ich meinen "Hybrid-Dekorator" so lassen. Ich finde es wäre etwas viel des Gute :D

Danke!

LG Glocke
deets

Jo, und schon hast du zweimal nen Transaktions-Handlings-Code geschrieben.

Das *mindeste* was du tun solltest ist einen Transaktions-Dekorator schreiben, und den dann *innerhalb* deiner deines Fehlerbehandlungs-Dekorators seinerseits zu verwenden, um die uebergebene Funktion zu dekorieren. Dann hast du den Code nur einmal.
glocke
User
Beiträge: 66
Registriert: Mittwoch 23. Februar 2011, 21:18

Naja bei der Kampfberechnung auftretende Fehler sollen in einer anderen Log-File gespeichert werden. Sicherlich könnte ich einem Exception-Handling-Dekorator noch einen Parameter mitgeben. Dann müsste ich bei den 284 Routen jeweils einen Parameter im Exception-Handling-Dekorator mitgeben.

Entspricht das dem was du meinst?

Code: Alles auswählen

def managed_transaction(func):
    def handle(*args, **kw):
        try:
            res = func(*args, **kw)
            print 'session.commit'
            return res
        except HTTPResponse:
            print 'session.commit also at redirect'
            raise
        except Exception as e:
            print 'session.rollback'
            raise
    return handle

def errorhandle(logger):
    def wrapper(func):
        def handling(*args, **kw):
            managed_func = managed_transaction(func)
            try:
                managed_func(*args, **kw)
            except HTTPResponse:
                raise
            except Exception as e:
                print 'logged "{0}" via {1}'.format(e, logger)
        return handling
    return wrapper

@errorhandle(logger='foo')
def bar():
    raise Exception('Test')

# ...
# inside CombatManagement
for combat in Combat.query.filter(...).all():
    combat.calculate()

# ...
# inside Combat class
    @errorhandle(logger='bar')
    def calculate(self):
        # ...
LG Glocke
deets

Ah, das mit der redirect-exception macht das auch haesslich. Naja, dann ist es wohl nicht wirklich moeglich das zu vereinigen.
glocke
User
Beiträge: 66
Registriert: Mittwoch 23. Februar 2011, 21:18

Ich lass die Sache einfach wie sie ist ^^
Antworten