In our project we recently ran into the problem that SQLAlchemy would block when creating a connection for logging to a database.
Set-up: Oracle server 11.2 cx_Oracle 6.1 SQLAlchemy 1.2.3 The example code for `connection_lockup.py` is: ====================================================================== """ Example script to demonstrate a lock-up when trying to create a second connection _while_ creating a first connection. Do _not_ modify the program to create the table and attempt to block in the same run. In this case, the lockup won't happen. Instead run the program first with `connection_lockup.py make_table` (unless the table already exists), then with `connection_lockup.py block`. """ import datetime import logging import os import sys import threading import sqlalchemy # Set connect string (starting with `oracle+cx_oracle://`) in environment. # I don't know if the problem also occurs with other dialects. CONNECT_STRING = os.getenv("CONNECT_STRING") # Schema and engine set-up. metadata = sqlalchemy.MetaData() log_table = sqlalchemy.Table( "log2", metadata, sqlalchemy.Column("id", sqlalchemy.Integer, sqlalchemy.schema.Sequence("log_id_seq"), primary_key=True), sqlalchemy.Column("timestamp_utc", sqlalchemy.TIMESTAMP(timezone=False), nullable=False), sqlalchemy.Column("level_name", sqlalchemy.Unicode(1000)), sqlalchemy.Column("message", sqlalchemy.Unicode(1000), nullable=False)) engine = sqlalchemy.create_engine(CONNECT_STRING) # ---------------------------------------------------------------------- def make_table(): log_table.create(engine, checkfirst=True) class DatabaseHandler(logging.Handler): """ Handler for logging to Oracle database """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Keep a reference to the engine for the log database. self._engine = engine # Make sure that at least one connection has been retrieved from the # engine before database logging is set up. # with self._engine.connect() as _connection: # pass # For preventing that the logging `INSERT` statement is also logged. thread_local = threading.local() thread_local.emit_lock = threading.Lock() self._emit_lock = thread_local.emit_lock def _emit(self, record): """ Emit the log record. """ # Some code causes a log message being an empty string. This wouldn't be # a problem, but Oracle implicitly converts this to `NULL`, which in # turn causes an `IntegrityError` when the log record is about to be # written to the database. Therefore, in this case set the message to a # sensible (non-empty) string. message = record.msg if not message: message = "(no log message)" else: message = message % record.args insert = log_table.insert() insert = insert.values( # According to the documentation # https://docs.python.org/3/library/logging.html#logrecord-attributes # the `created` attribute is filled from `time.time()`, which # uses local time. Therefore, use `utcfromtimestamp` to get # an UTC timestamp. timestamp_utc=datetime.datetime.utcfromtimestamp(record.created), level_name=record.levelname, message=message) # Run `INSERT` in a transaction. See # http://docs.sqlalchemy.org/en/latest/core/connections.html#using-transactions with self._engine.begin() as connection: connection.execute(insert) def emit(self, record): """ Emit the log record. Make sure that emitting the log record isn't invoked recursively, i. e. when SQLAlchemy itself would log something while inserting the log record into the database. """ is_acquired = self._emit_lock.acquire(blocking=False) if not is_acquired: # An "outer" call of `emit` acquired the lock before. Since we don't # want any recursive calls, return immediately. return # Don't use a `with` statement here because this would try to acquire # the lock. See # https://docs.python.org/3/library/threading.html#using-locks-conditions-and-semaphores-in-the-with-statement try: self._emit(record) finally: self._emit_lock.release() def block(): logger = logging.getLogger("sqlalchemy") logger.addHandler(DatabaseHandler()) logger.setLevel(logging.INFO) # Blocks. # # For this to "work", this must be the first connection created for the # engine. connection = engine.connect() def main(command): if command == "make_table": make_table() elif command == "block": block() else: print("invalid command {!r}".format(command)) if __name__ == "__main__": if len(sys.argv) != 2: print("usage: connection_lockup.py make_table|block") sys.exit() command = sys.argv[1] main(command) ====================================================================== You need to run the program at least twice. Once to create the log table, with $ export CONNECT_STRING='oracle+cx_oracle://...' $ python connection_lockup.py make_table then with $ python connection_lockup.py block to see SQLAlchemy block. When pressing ctrl-c, the result is the following traceback (interspersed with some comments). ====================================================================== $ python connection_lockup.py block ^CTraceback (most recent call last): File "connection_lockup.py", line 142, in <module> main(command) File "connection_lockup.py", line 132, in main block() File "connection_lockup.py", line 125, in block connection = engine.connect() File "/home/.virtualenvs/connection_lockup/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 2102, in connect return self._connection_cls(self, **kwargs) File "/home/.virtualenvs/connection_lockup/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 90, in __init__ if connection is not None else engine.raw_connection() File "/home/.virtualenvs/connection_lockup/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 2188, in raw_connection self.pool.unique_connection, _connection) File "/home/.virtualenvs/connection_lockup/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 2158, in _wrap_pool_connect return fn() File "/home/.virtualenvs/connection_lockup/lib/python3.6/site-packages/sqlalchemy/pool.py", line 345, in unique_connection return _ConnectionFairy._checkout(self) [This section repeats further down for the connection attempt in the database log handler.] File "/home/.virtualenvs/connection_lockup/lib/python3.6/site-packages/sqlalchemy/pool.py", line 784, in _checkout fairy = _ConnectionRecord.checkout(pool) File "/home/.virtualenvs/connection_lockup/lib/python3.6/site-packages/sqlalchemy/pool.py", line 532, in checkout rec = pool._do_get() File "/home/.virtualenvs/connection_lockup/lib/python3.6/site-packages/sqlalchemy/pool.py", line 1189, in _do_get self._dec_overflow() File "/home/.virtualenvs/connection_lockup/lib/python3.6/site-packages/sqlalchemy/util/langhelpers.py", line 66, in __exit__ compat.reraise(exc_type, exc_value, exc_tb) File "/home/.virtualenvs/connection_lockup/lib/python3.6/site-packages/sqlalchemy/util/compat.py", line 187, in reraise raise value File "/home/.virtualenvs/connection_lockup/lib/python3.6/site-packages/sqlalchemy/pool.py", line 1186, in _do_get return self._create_connection() File "/home/.virtualenvs/connection_lockup/lib/python3.6/site-packages/sqlalchemy/pool.py", line 350, in _create_connection return _ConnectionRecord(self) File "/home/.virtualenvs/connection_lockup/lib/python3.6/site-packages/sqlalchemy/pool.py", line 477, in __init__ self.__connect(first_connect_check=True) File "/home/.virtualenvs/connection_lockup/lib/python3.6/site-packages/sqlalchemy/pool.py", line 677, in __connect exec_once(self.connection, self) [End of repeating section] File "/home/.virtualenvs/connection_lockup/lib/python3.6/site-packages/sqlalchemy/event/attr.py", line 271, in exec_once self(*args, **kw) File "/home/.virtualenvs/connection_lockup/lib/python3.6/site-packages/sqlalchemy/event/attr.py", line 281, in __call__ fn(*args, **kw) File "/home/.virtualenvs/connection_lockup/lib/python3.6/site-packages/sqlalchemy/util/langhelpers.py", line 1334, in go return once_fn(*arg, **kw) File "/home/.virtualenvs/connection_lockup/lib/python3.6/site-packages/sqlalchemy/engine/strategies.py", line 183, in first_connect dialect.initialize(c) File "/home/.virtualenvs/connection_lockup/lib/python3.6/site-packages/sqlalchemy/dialects/oracle/cx_oracle.py", line 663, in initialize super(OracleDialect_cx_oracle, self).initialize(connection) File "/home/.virtualenvs/connection_lockup/lib/python3.6/site-packages/sqlalchemy/dialects/oracle/base.py", line 1091, in initialize super(OracleDialect, self).initialize(connection) File "/home/.virtualenvs/connection_lockup/lib/python3.6/site-packages/sqlalchemy/engine/default.py", line 267, in initialize self._get_default_schema_name(connection) File "/home/.virtualenvs/connection_lockup/lib/python3.6/site-packages/sqlalchemy/dialects/oracle/base.py", line 1179, in _get_default_schema_name connection.execute('SELECT USER FROM DUAL').scalar()) File "/home/.virtualenvs/connection_lockup/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 942, in execute return self._execute_text(object, multiparams, params) File "/home/.virtualenvs/connection_lockup/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1104, in _execute_text statement, parameters [Log call from SQLAlchemy] File "/home/.virtualenvs/connection_lockup/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1151, in _execute_context self.engine.logger.info(statement) [In `logging`] File "/usr/lib/python3.6/logging/__init__.py", line 1306, in info self._log(INFO, msg, args, **kwargs) File "/usr/lib/python3.6/logging/__init__.py", line 1442, in _log self.handle(record) File "/usr/lib/python3.6/logging/__init__.py", line 1452, in handle self.callHandlers(record) File "/usr/lib/python3.6/logging/__init__.py", line 1514, in callHandlers hdlr.handle(record) File "/usr/lib/python3.6/logging/__init__.py", line 863, in handle self.emit(record) [In the database log handler] File "connection_lockup.py", line 112, in emit self._emit(record) File "connection_lockup.py", line 92, in _emit with self._engine.begin() as connection: [Connection attempt in the database log handler] File "/home/.virtualenvs/connection_lockup/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1992, in begin conn = self.contextual_connect(close_with_result=close_with_result) File "/home/.virtualenvs/connection_lockup/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 2123, in contextual_connect self._wrap_pool_connect(self.pool.connect, None), File "/home/.virtualenvs/connection_lockup/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 2158, in _wrap_pool_connect return fn() File "/home/.virtualenvs/connection_lockup/lib/python3.6/site-packages/sqlalchemy/pool.py", line 403, in connect return _ConnectionFairy._checkout(self) [Repeating section of traceback, see above] File "/home/.virtualenvs/connection_lockup/lib/python3.6/site-packages/sqlalchemy/pool.py", line 784, in _checkout fairy = _ConnectionRecord.checkout(pool) File "/home/.virtualenvs/connection_lockup/lib/python3.6/site-packages/sqlalchemy/pool.py", line 532, in checkout rec = pool._do_get() File "/home/.virtualenvs/connection_lockup/lib/python3.6/site-packages/sqlalchemy/pool.py", line 1189, in _do_get self._dec_overflow() File "/home/.virtualenvs/connection_lockup/lib/python3.6/site-packages/sqlalchemy/util/langhelpers.py", line 66, in __exit__ compat.reraise(exc_type, exc_value, exc_tb) File "/home/.virtualenvs/connection_lockup/lib/python3.6/site-packages/sqlalchemy/util/compat.py", line 187, in reraise raise value File "/home/.virtualenvs/connection_lockup/lib/python3.6/site-packages/sqlalchemy/pool.py", line 1186, in _do_get return self._create_connection() File "/home/.virtualenvs/connection_lockup/lib/python3.6/site-packages/sqlalchemy/pool.py", line 350, in _create_connection return _ConnectionRecord(self) File "/home/.virtualenvs/connection_lockup/lib/python3.6/site-packages/sqlalchemy/pool.py", line 477, in __init__ self.__connect(first_connect_check=True) File "/home/.virtualenvs/connection_lockup/lib/python3.6/site-packages/sqlalchemy/pool.py", line 677, in __connect exec_once(self.connection, self) [SQLAlchemy blocks here, waiting for the lock. This seems to be the only place in the SQLAlchemy code where this lock is used.] File "/home/.virtualenvs/connection_lockup/lib/python3.6/site-packages/sqlalchemy/event/attr.py", line 268, in exec_once with self._exec_once_mutex: KeyboardInterrupt ====================================================================== It's worth noting that this blocking only happens if no other connection has been created for the engine before (which also implies that nothing was logged to the database when `engine.connect` is called for the first time). Our current workaround is to make sure that at least one connection is created before the first log call happens. The above code does this in `DatabaseHandler.__init__` (commented out). If you uncomment the `with` block there, no blocking happens. Do you have any advice? Should we stick with the workaround? Is there anything else we should do? Do you consider this problem a bug in SQLAlchemy? Admittedly it's an edge case. At the moment, I can't think of any situation that would try a first connection of an engine from the same thread recursively. On the other hand, I think that logging to the database as above is a legitimate use case. What do you think? Best regards, Stefan -- SQLAlchemy - The Python SQL Toolkit and Object Relational Mapper http://www.sqlalchemy.org/ To post example code, please provide an MCVE: Minimal, Complete, and Verifiable Example. See http://stackoverflow.com/help/mcve for a full description. --- You received this message because you are subscribed to the Google Groups "sqlalchemy" group. To unsubscribe from this group and stop receiving emails from it, send an email to sqlalchemy+unsubscr...@googlegroups.com. To post to this group, send email to sqlalchemy@googlegroups.com. Visit this group at https://groups.google.com/group/sqlalchemy. For more options, visit https://groups.google.com/d/optout.