Bottle & Elixir: Frage zum "richtigen Zusammenspiel"

Installation und Anwendung von Datenbankschnittstellen wie SQLite, PostgreSQL, MariaDB/MySQL, der DB-API 2.0 und sonstigen Datenbanksystemen.
Antworten
glocke
User
Beiträge: 66
Registriert: Mittwoch 23. Februar 2011, 21:18

Hi,

mein Browsergame liefert mir derzeit immer wieder gerne einen `OperationalError`:
OperationalError: (OperationalError) (1205, 'Lock wait timeout exceeded; try restarting transaction') 'UPDATE `Character` SET weapon_id=%s WHERE `Character`.id = %s' (10L, 1L)
Offenbar schließt er die Transaktion nicht richtig ab und das Feld bleibt gelockt.

Hintergrund: Mit der use-Methode der Character-Instanz wird self.weapon = item ausgeführt:

Code: Alles auswählen

if isinstance(item, Weapon):
    if item.twohanded:
        # reset offhand
        self.offhand = None
    self.weapon = item
    # other stuff via elif
Weapon ist eine von Item(Entity) abgeleitete Klasse und self dabei die Referenz auf eine Character-Instanz (Character(Entity), wir befinden uns hier in Character.use(self, item)), und die hat u.A. ManyToOne-Attribute mit Namen weapon und offhand (die sich wiederrum auf Weapon(Item) und Offhand(Item) beziehen). In der (mit route dekorierten) Funktion - in der, der char.use(item)-Call stattfindet - folgt darauf unmittelbar session.commit() und die Rückgabe des mit Werten gefüllten Templates.

Verwende ich use um z.B. self.armor (Armor(Item)) zu setzen, habe ich den Fehler nicht. Der einzige Unterschied im Code ist dort, dass kein self.offhand oder ähnliches überprüft werden muss (bei der Waffe brauche ich es um zu verhindern, dass man Zweihandwaffe und Schild trägt).

Wenn ich dann versuche eine weitere Datenbankabfrage zu erzeugen (z.B. ein Select) erhalte ich:
This Session's transaction has been rolled back due to a previous exception during flush. To begin a new transaction with this Session, first issue Session.rollback(). Original exception was: (OperationalError) (1205, 'Lock wait timeout exceeded; try restarting transaction') 'UPDATE Character SET weapon_id=%s WHERE Character.id = %s' (10L, 1L)
Spätestens an der Stelle wird es unangenehm -.-' Kille ich den Server, ist das Problem weg. Allerdings lässt es sich nach wenigen Sekunden reproduzieren. Wenn ich ein Seitenladen abbreche, meldet bottle mir in der Konsole eine abgefangene Broken pipe (soweit klar). Offensichtlich wird dabei der Thread (der für die Anfrage zuständig ist) gekillt. Allerdings bleibt die Datenbankaktion (z.B. mitten im gelockten Update) stehen und lässt das Feld gelockt. Was kann ich dagegen tun? :-?

Führe ich die gleichen Aktionen in der Python-Konsole aus, läuft alles fehlerfrei: keine Transaktionen blockieren oder sonstwas.

Um inhaltliche Denkfehler zu vermeiden, hier mein das Grobkonzept für das Zusammenspiel von bottle-Routen und DB-Transaktionen. Die bottle-Routen laufen inhaltlich so ab:
  • versuche Authentifizierung anhand Session-ID (account.sid von Account(Entity)), sonst Umleitung auf Fehlerseite
  • führe die entsprechenden Dinge mit der Character-Instanz der eigenen Account-Instanz durch (account.character dient als Referenz) z.B. die ItemID aus dem Post lesen, das entsprechende Item dazu durch ein Query finden, prüfen ob item in character.inventory und dann character.use(item).
  • wenn alles erfolgreich, dann session.commit() und das entsprechende Template zurückgeben (gefüllt mit den Daten)
  • wenn was schiefgeht: session.rollback() und den Fehler-Traceback in ne Log-File schreiben.
Hier mal noch der Codeausschnitt dazu:

Code: Alles auswählen

@post(u'/game/inventory/:sid/use/')
def game_inventory_use(sid):
    try:
        player = Player.AuthPost(sid)
        item_id = int(request.POST[u'item_id'])

        # try use item
        item = Item.query.filter_by(id=item_id).first()
        if item is None or item not in player.character.inventory:
            return u'Ungültiges Item'
        try:
            player.character.use(item)
            return 'Nichts geschieht.'
        except Report as e:
            session.commit()
            return unicode(e) # event
    except Exception as e:
        if isinstance(e, HTTPResponse): raise
            session.rollback()
            syslog.log(e)
            redirect(u'/internal_error/')
Zur Erklärung:
  • AuthPost führt die Authentifizierung durch (Umleitung erfolgt innerhalb der Methode)
  • das Try-Except-Block hat den Hintergrund, dass ich "Spielereignisse" (wie "Christian verwendet Kurzschwert.") von einer Funktion (mit einem zufälligen der gegebenen Texte) aus Report(Exception) erzeugen lassen (auch um aus der Funktion "schneller"). Ich habe mich gegen return entschieden, da ich return für "Nicht-Spielereignisse" verwende (sry wenn's unverständlich ist wegen der Exception :D ). Auf jeden Fall führt es (wenn ich einen string returne) zum gleichen Problem; scheint also nicht daran zu liegen
  • das Abfangen aller weiteren Exceptions dient dem Loggen von Fehlern. Dazu hab ich ne Logger-Klasse die thread-safe in eine Datei schreibt. (das mit dem isinstance(e, HTTPResponse) brauche ich um bottle's redirects nicht fälschlicher Weise abzufangen, die sind offensichtlich auch von Exception abgeleitet)
LG Glocke
deets

Fuer sowas solltest du eine Session-Middleware wie repoze.tm2 verwenden, oder dir einen Transaktions-Dekorator schreiben. Vielleicht hat auch Bottle sowas wie Filter/Interceptoren.

Denn das ist alles Boilerplate-Code ,den du nicht jedes mal neu schreiben (und debuggen!) willst fuer einen Action.

Der Rest von deine Ausfuehrungen klingt etwas Konfus. Vor allem der Exception-Kram, da scheint mir etwas im argen zu liegen, wenn du Exception nicht zur Fehlerbehandlung, sondern fuer Kontrollfluss verwendest. Das *kann* man gelegentlich machen, meistens aber ist es falsch. Und fuehrt uA zu solchen Problemen, weil du komplexe Code-Pfade erzeugst.

Last but not least: das Python-logging Modul regelst konkurriernde Zugriffe auf Logfiles fuer dich. Ich fuerchte, dass du entweder etwas eigenes benutzt, oder dir nutzlos arbeit machst, wenn du extra von "thread-safe logging ..." redest.
glocke
User
Beiträge: 66
Registriert: Mittwoch 23. Februar 2011, 21:18

deets hat geschrieben:Fuer sowas solltest du eine Session-Middleware wie repoze.tm2 verwenden, oder dir einen Transaktions-Dekorator schreiben. Vielleicht hat auch Bottle sowas wie Filter/Interceptoren.

Denn das ist alles Boilerplate-Code ,den du nicht jedes mal neu schreiben (und debuggen!) willst fuer einen Action.
"Transaktions-Dekorator" klingt interessant. Kannst du dazu nähere Ausführungen machen oder mir nen Link geben?
deets hat geschrieben:Der Rest von deine Ausfuehrungen klingt etwas Konfus.
Was genau (außer dem Exception-Kram) klingt konfus?
deets hat geschrieben:Vor allem der Exception-Kram, da scheint mir etwas im argen zu liegen, wenn du Exception nicht zur Fehlerbehandlung, sondern fuer Kontrollfluss verwendest. Das *kann* man gelegentlich machen, meistens aber ist es falsch. Und fuehrt uA zu solchen Problemen, weil du komplexe Code-Pfade erzeugst.
Also schreib ich das mal besser um :D

/EDIT: Hab die Geschichte mit den (quasi unnötigen) Exceptions jetzt umgeschrieben (die "Ereignisse" in String-Form werden mittels return nun zurückgegeben). Das genannte Problem besteht weiterhin :-(
deets hat geschrieben:Last but not least: das Python-logging Modul regelst konkurriernde Zugriffe auf Logfiles fuer dich. Ich fuerchte, dass du entweder etwas eigenes benutzt, oder dir nutzlos arbeit machst, wenn du extra von "thread-safe logging ..." redest.
Ich schau mir das logging-Modul mal an.

LG Glocke
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

@glocke: Du hättest ruhig die beiden Threads auf uu.de verlinken können - als Kontext könnte so etwas sicherlich hilfreich sein ;-)
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
glocke
User
Beiträge: 66
Registriert: Mittwoch 23. Februar 2011, 21:18

Du meinst sicher http://forum.ubuntuusers.de/topic/pytho ... -exceeded/ und http://forum.ubuntuusers.de/topic/pytho ... nd-c-linq/ ?

Der erste hat 1:1 den gleichen Inhalt wie hier, und der zweite hat sich inzwischen gegessen :)

LG Glocke

/EDIT: Ich hab' mal einige der genannten use-Aktionen durchgeführt, ohne die Anfrage abzubrechen (teilweise mit enormer Wartezeit, daher hatte ich sonst abgebrochen). Es kommt zu keinem 'Lock Wait Timeout'! Allerdings kann ich nicht davon ausgehen, dass ein späterer User jedes Seiteladen bzw. jedem Ajax-Request durchlaufen lässt, "wenn es mal wieder länger dauert". Da die Wartezeiten stark schwanken, bin ich mir nicht sicher, was der Grund ist.
glocke
User
Beiträge: 66
Registriert: Mittwoch 23. Februar 2011, 21:18

@deets: Zielst du mit Transaktions-Dekorator auf soetwas ab?

Code: Alles auswählen

#!/usr/bin/python
# -*- coding: utf-8 -*-
def begin():
    print 'begin'
def commit():
    print 'commit'
def rollback():
    print 'rollback'
def commit_on_success(func):
    '''
    Führe commit() aus, falls die dekorierte Funktion keine Exception
    wirft. Ansonsten wird rollback() aufgerufen.
    '''
    def _commit_on_success(*args, **kw):
        begin()
        try:
            res = func(*args, **kw)
        except Exception, e:
            rollback()
            raise # Re-raise (aufgefangene Exception erneut werfen)
        else:
            commit()
        return res
    return _commit_on_success

@commit_on_success
def foo(do_raise):
    if do_raise:
        raise Exception()

foo(False)  # --> commit()
foo(True)   # --> rollback()
Quelle: http://www.thomas-guettler.de/vortraege ... #link_21.3

LG Glocke
glocke
User
Beiträge: 66
Registriert: Mittwoch 23. Februar 2011, 21:18

Gut :) Aber inwiefern hilft mir das die Transaktion ohne Deadlock abzubrechen, wenn (vom Server) ein "Broken Pipe"-Fehler abgefangen wird - und die DB-Transaktion im Hintergrund im dümmsten Fall vor dem unlock abbricht. Beim Abfangen der Broken Pipe wird keine Exception ausgelöst (hab es nochmal getestet).

Das würde sich dann so verhalten (denke ich):

Code: Alles auswählen

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

@managed_transaction
def test():
    print 'Data locked'
    while True: # not-ending transaction
        pass
    print 'Data unlocked'

Code: Alles auswählen

Python 2.7.3 (default, Aug  1 2012, 05:16:07) 
[GCC 4.6.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from deko_test import test
>>> test()
Data locked
^CTraceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "deko_test.py", line 24, in _managed_transaction
    res = func(*args, **kw)
  File "deko_test.py", line 34, in test
    while True: # not-ending transaction
KeyboardInterrupt
>>> 
Der KeyboardInterrupt wäre dann das Äquivalent zum abgebrochenen Seiteladen bzw. der abgefangenen "Broken Pipe".

Naja ich kann mir den Fehler nur so erklären.

LG Glocke :)
BlackJack

@glocke: Mach mal aus der `Exception` eine `BaseException`, denn davon erbt `KeyboardInterrupt`.
glocke
User
Beiträge: 66
Registriert: Mittwoch 23. Februar 2011, 21:18

Das bringt mir in dem Falle nichts. Die KeyboardInterrupt wird beim eigentlichen Problem nicht ausgelöst - die habe ich hier nur verwendet, um quasi den "Prozess abzubrechen".

Das Problem scheint aus meiner Sicht an 2 Punkten zu liegen:
  • Ich finde im bottle-Framework keine Möglichkeit, nach einer Broken Pipe noch etwas mit den Daten des Threads (in dem Falle der DB-Session) zu tun - quasi "aufzuräumen". Da wir aber hier im Datenbanken-Unterforum sind: http://www.python-forum.de/viewtopic.php?f=7&t=29950
  • Und selbst wenn ich etwas finde, weiß ich noch nicht wie ich den Lock (den SQL-Alchemy offensichtlich setzt) rückgängig mache. Ich denke das ist hier eher zu thematisieren.
LG Glocke
Antworten