On Wed, Feb 21, 2018 at 7:16 AM, Stefan Schwarzer
<[email protected]> wrote:
> In our project we recently ran into the problem that SQLAlchemy would
> block when creating a connection for logging to a database.
>
>
>     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))

>
> 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.

I can come up with other things that can break based on the idea that
you're doing a full connection + execute sequence within the log
handler deep within that engine's own processes, such as, it can cause
connection pool exhaustion when you're working with the last
connection available, then your logger has no connection, it goes to
get one and...hang.      I can reproduce this with your script (set
pool_size=1, max_overflow=0, set up logging and run an execute() so it
actually logs, and it hangs for 30 seconds until the timeout and
crashes, see stack trace below), so your approach is not safe in any
case.

It would be a lot simpler and robust if your log handler just used its
own Engine (make a new one from the engine.url you already have).
That way you can lose all that re-entrant mutex stuff too.




Traceback (most recent call last):
  File "test.py", line 134, in <module>
    main(command)
  File "test.py", line 124, in main
    block()
  File "test.py", line 117, in block
    connection.execute("select 1")
  File "/home/classic/dev/sqlalchemy/lib/sqlalchemy/engine/base.py",
line 942, in execute
    return self._execute_text(object, multiparams, params)
  File "/home/classic/dev/sqlalchemy/lib/sqlalchemy/engine/base.py",
line 1104, in _execute_text
    statement, parameters
  File "/home/classic/dev/sqlalchemy/lib/sqlalchemy/engine/base.py",
line 1151, in _execute_context
    self.engine.logger.info(statement)
  File "/home/classic/dev/sqlalchemy/lib/sqlalchemy/log.py", line 109, in info
    self.log(logging.INFO, msg, *args, **kwargs)
  File "/home/classic/dev/sqlalchemy/lib/sqlalchemy/log.py", line 155, in log
    self.logger._log(level, msg, args, **kwargs)
  File "/usr/lib64/python3.6/logging/__init__.py", line 1442, in _log
    self.handle(record)
  File "/usr/lib64/python3.6/logging/__init__.py", line 1452, in handle
    self.callHandlers(record)
  File "/usr/lib64/python3.6/logging/__init__.py", line 1514, in callHandlers
    hdlr.handle(record)
  File "/usr/lib64/python3.6/logging/__init__.py", line 863, in handle
    self.emit(record)
  File "test.py", line 102, in emit
    self._emit(record)
  File "test.py", line 82, in _emit
    with self._engine.begin() as connection:
  File "/home/classic/dev/sqlalchemy/lib/sqlalchemy/engine/base.py",
line 1992, in begin
    conn = self.contextual_connect(close_with_result=close_with_result)
  File "/home/classic/dev/sqlalchemy/lib/sqlalchemy/engine/base.py",
line 2123, in contextual_connect
    self._wrap_pool_connect(self.pool.connect, None),
  File "/home/classic/dev/sqlalchemy/lib/sqlalchemy/engine/base.py",
line 2158, in _wrap_pool_connect
    return fn()
  File "/home/classic/dev/sqlalchemy/lib/sqlalchemy/pool.py", line
403, in connect
    return _ConnectionFairy._checkout(self)
  File "/home/classic/dev/sqlalchemy/lib/sqlalchemy/pool.py", line
784, in _checkout
    fairy = _ConnectionRecord.checkout(pool)
  File "/home/classic/dev/sqlalchemy/lib/sqlalchemy/pool.py", line
532, in checkout
    rec = pool._do_get()
  File "/home/classic/dev/sqlalchemy/lib/sqlalchemy/pool.py", line
1182, in _do_get
    (self.size(), self.overflow(), self._timeout), code="3o7r")
sqlalchemy.exc.TimeoutError: QueuePool limit of size 1 overflow 0
reached, connection timed out, timeout 30 (Background on this error
at: http://sqlalche.me/e/3o7r)




>
> 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 [email protected].
> To post to this group, send email to [email protected].
> Visit this group at https://groups.google.com/group/sqlalchemy.
> For more options, visit https://groups.google.com/d/optout.

-- 
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 [email protected].
To post to this group, send email to [email protected].
Visit this group at https://groups.google.com/group/sqlalchemy.
For more options, visit https://groups.google.com/d/optout.

Reply via email to