SQLAlchemy: Timeout nach 30 Sekunden?

Installation und Anwendung von Datenbankschnittstellen wie SQLite, PostgreSQL, MySQL, der DB-API 2.0 und sonstigen Datenbanksystemen.
Antworten
Benutzeravatar
Sophus
User
Beiträge: 1092
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Freitag 2. Juni 2017, 15:54

Hallo Leute,

ich habe ein merkwürdiges Problem. Wenn ich einige Datensätze in die Datenbank hinzufüge, bekomme ich nach einer Weile folgende Meldung:
TimeoutError: QueuePool limit of size 5 overflow 10 reached, connection timed out, timeout 30
Die Meldung ist deshalb merkwürdig, weil ich die gegebene Session mit dem With-Context arbeite bzw. behandle. Das heißt, nach jeder Benutzung wird die Session automatisch geschlossen, korrekt? Da mein Projekt mittlerweile sehr umfangreich geworden ist, habe ich als Beispiel nur einen kleinen, nicht ausführbaren Ausschnitt mit gebracht. Als Zusatzinformation: Die Einträge in die Datenbank findet in einem Neben-Thread statt, also nicht im Hauptthread. Denn ich möchte nicht, dass meine GUI einfriert, während SQLAlchemy arbeitet. Das heißt, dass Laden, Speichern, Löschen und Bearbeiten von Datensätzen werden auf NebenThread verlegt. Aber der Nebenthread wird nach Gebrauch wieder zum Löschen freigegeben.

Code: Alles auswählen

[...]
    def add_film_genre(self, genre=None):

        test = FILM_GENRE

    	with self._session_scope as session:
    		
    		session.add(FILM_GENRE(Genre=genre))

    		session.commit()

    	return
[...]
Soll ich etwa zur Sicherheit session.close() innerhalb der With-Anweisung schreiben, damit nach der commit()-Methode die Session tatsächlich geschlossen wird, ehe die With-Anweisung verlassen wird?


Meine Engine ist wie folgt konfiguriert:

Code: Alles auswählen

[...]
        url = '{}+{}://{}:{}@{}:{}/{}'.format(
           self.dbms, self.dbdriver, self.dbuser, self.dbuser_pwd, self.db_server_host, self.dbport, self.db_name)

        self._Engine = create_engine(url, encoding='utf8', echo=True)
[...]
Die Sache ist einfach, dass das Problem nicht nach dem ersten Datensatz auftaucht, sondern erst (sagen wir mal ungefähr) nach 20 Eingaben. Wenn ich also beispielsweise 20 Datensätze abgespeichert habe, fängt mein Programm an zu stocken, die GUI friert ein, und dann bekomme ich diesen oben genannten Traceback.
Benutzeravatar
noisefloor
User
Beiträge: 2495
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: Görgeshausen
Kontaktdaten:

Freitag 2. Juni 2017, 20:12

Hallo,

AFAIK verwendet und verwaltet SQLAlchemy eine Connection-Pool, der für die Verbindungen zu Datenbank genutzt wird. Baut man eine neue Verbindung auf, wird eine "Connection" aus dem Pool entnommen. Wenn alles fertig ist, wird die "Conneciton" zurück gelegt.

Wenn dein Pool leer läuft, dann hält irgendwas irgendwo die Verbindung offen. Wo, lässt sich (zumindest für mich) anhand des Codes nicht sagen.

Vermutung: jeder neue Thread bekomt ein Connection mit, aber die Threads werden - entgegen deiner Aussage / Vermutung eben _nicht_ geschlossen?

Hast du das ganze mal ohne GUI und Threads getestet?

Gruß, noisefloor
BlackJack

Freitag 2. Juni 2017, 20:27

Oder vielleicht werden auch zu viele Threads parallel gestartet die etwas eintragen sollen/wollen. Bleibt aber alles nur raten.
Benutzeravatar
Sophus
User
Beiträge: 1092
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Freitag 2. Juni 2017, 20:31

Hallo noisefloor,

ich gebe meine Threads immer frei, damit sie nachher von der garbage collection von Python abgeräumt werden können. Hier ein Ausschnitt:

Code: Alles auswählen

task_thread.finished.connect(task_thread.deleteLater)
Mit der deleteLater()-Methode sollte der Thread nach der Benutzung abgeräumt werden. Frage: Braucht Python "lange" bis der aufgeräumt hat? Nicht das Python den Thread eine Weile behält und das Programm deswegen viele Verbindungen aufbaut bzw. aufrecht erhält?

Meine Vermutung habe ich woanders gefunden - zumindest glaube ich das. Da ich ja mit QThread arbeite, habe ich mal nachgeschlagen, ob ein Session-Objekt threadsicher ist. Nein ist es nicht. Ich fand dann folgenden Satz:
"The Session object is entirely designed to be used in a non-concurrent fashion, which in terms of multithreading means "only in one thread at a time" .. some process needs to be in place such that mutltiple calls across many threads don’t actually get a handle to the same session. We call this notion thread local storage."
Also bin ich auf folgende Seite gelandet: http://docs.sqlalchemy.org/en/latest/or ... xtual.html

Demnach habe ich meine Verbindung etwas umgebaut:

Code: Alles auswählen

[...]
        url = '{}+{}://{}:{}@{}:{}/{}'.format(
           self.dbms, self.dbdriver, self.dbuser, self.dbuser_pwd, self.db_server_host, self.dbport, self.db_name)

        self._Engine = create_engine(url, encoding='utf8', echo=True)

        '''
            Set up the session and store a sessionmaker for this db connection object
        '''

        #self._Session = sessionmaker(bind=self._Engine)
        self.session = None

        self._session_factory = sessionmaker(bind=self._Engine)

        '''
            Session registry is established
        '''
        self._Session = scoped_session(self._session_factory)[...]
Ich habe mir die scoped_session-Klasse herangezogen. Ich habe einige Testläufe durchgeführt und bisher bekam ich keine Fehlermeldungen. Es kann sein, dass meine Vermutung falsch liegt. Ich werde mal im Auge behalten und dann berichten, wenn ich falsch liege und eine Meldung bekomme.
Sirius3
User
Beiträge: 8603
Registriert: Sonntag 21. Oktober 2012, 17:20

Samstag 3. Juni 2017, 09:47

@Sophus: Du hast am Anfang eben nicht immer wieder eine neue Session erzeugt, sondern nur jeweils eine neue Transaktion. Das sind unterschiedliche Dinge. Der commit ist überflüssig, weil wenn eine Transaktion, die mit with erzeugt wird, erfolgreich durchlaufen wird, sie automatisch kommittiert und im Fehlerfall automatisch zurückgerollt wird. Das return ist überflüssig, weil eine Funktion die nichts zurückliefert am Ende der Funktion automatisch zum Aufrufer zurückkehrt.
Benutzeravatar
Sophus
User
Beiträge: 1092
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Samstag 3. Juni 2017, 12:08

@Siriuse: Das verstehe ich nicht. Denn self._session_scope ist ein Session-Objekt, der erzeugt wurde, nachdem die Login-Daten zur Datenbank richtig eingegebene wurde. Bei jeder With-Anweisung sollte jedesmal eine Verbindung durch die neue Session hergestellt werden, nachdem die Session durch die With-Anweisung geschlossen wird.

Meine SessionScope()-Klasse sieht nämlich so aus. Nachdem die Zugangsdaten erfolgreich eingegeben wurden, wird das SessionScope()-Objekt erstellt. Und da ich diese Klasse als With-Context-Manager konstruiert habe, wird in der __enter__()-Methode jedesmal eine neue Session erstellt, sobald ich einen Datensatz abspeichern, löschen oder bearbeiten möchte. Von daher verstehe ich deine Anmerkung nicht ganz, Sirius3.

Code: Alles auswählen

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm import scoped_session

from sqlalchemy.orm import relationship, backref

from sqlalchemy import inspect

from sqlalchemy import Table, MetaData 

class SessionScope(object):
    def __init__(self, dbms=None, dbdriver=None,
                 dbuser=None, dbuser_pwd=None,
                 db_server_host=None, dbport=None, db_name=None,
                 admin_database=None):

        self.dbms = dbms
        self.dbdriver = dbdriver
        self.dbuser = dbuser
        self.dbuser_pwd = dbuser_pwd
        self.db_server_host = db_server_host
        self.dbport = dbport
        self.db_name = db_name
        self.admin_database = admin_database
        
        url = '{}+{}://{}:{}@{}:{}/{}'.format(
           self.dbms, self.dbdriver, self.dbuser, self.dbuser_pwd, self.db_server_host, self.dbport, self.db_name)

        '''
            Currently the echo is turned on to see the auto-generated SQL.

            That is, the Engine is a factory for connections as well as a pool of connections, 
            not the connection itself. When you say in this case close(), 
            the connection is returned to the connection pool within the Engine, not actually closed.

            So the self._Engine will not use connection pool if you set poolclass=NullPool. 
            So the connection (SQLAlchemy session) will close directly after session.close()
            that means, if you set poolclass=NullPool each call to close() will close the underlying DBAPI connection.
        '''
        self._Engine = create_engine(url, encoding='utf8', echo=True)

        '''
            Set up the session and store a sessionmaker for this db connection object
        '''

        #self._Session = sessionmaker(bind=self._Engine)
        self.session = None

        self._session_factory = sessionmaker(bind=self._Engine)

        '''
            Session registry is established
        '''
        self._Session = scoped_session(self._session_factory)

    def __enter__(self):
        '''
            Now all calls to Session() will create a thread-local session.
            That means, you can now use self.session to run multiple queries, etc.
            The registry is *optionally* starts called upon explicitly to create
            a Session local to the thread and/or request. That why we return self.session
        '''
        self.session = self._Session()
        return self.session

    def __exit__(self, exception, exc_value, traceback):

        try:
            if exception:

                self.session.rollback()
            else:

                self.session.commit()

        finally:

            self.session.close()
Benutzeravatar
Sophus
User
Beiträge: 1092
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Samstag 3. Juni 2017, 12:52

Sirius3 hat geschrieben:@Sophus: Der commit ist überflüssig, weil wenn eine Transaktion, die mit with erzeugt wird, erfolgreich durchlaufen wird, sie automatisch kommittiert und im Fehlerfall automatisch zurückgerollt wird. Das return ist überflüssig, weil eine Funktion die nichts zurückliefert am Ende der Funktion automatisch zum Aufrufer zurückkehrt.
Du hast Recht. Im With-Context-Manager ist der commit() tatsächlich überflüssig. Danke. Frage: Brauche ich im With-Context-Manager auch keinen flush(), damit die Daten vorher schon mal an die Datenbank gesendet wird? Oder wäre das auch überflüssig? Denn ich vermute mal, dass der flush() durch den automatischen commit() ausgeführt wird.

Das mit den leeren return ist nur eine dumme Angewohnheit von mir. Ich fühle mich dabei wohler, weil ich mir einrede, dass durch
return auch definitiv und tatsächlich beendet wird. Durch die return fühlt sich das Beenden der Funktion so rigoros an. Daher diese dumme Angewohnheit.
Antworten