Skip to content

SqlAlchemy aiosqlite wrapper doesn't close the aiosqlite connection object (hanging thread) #13039

Closed

Description

@amyreese

Describe the bug

The aiosqlite dialect/wrapper never calls/awaits the underlying connection's .close() method, instead depending on having set connection.daemon = True at connection time with the expectation that aiosqlite's thread will simply exit whenever the program using sqlalchemy exits.

This a) potentially produces multiple stale threads if the user creates multiple connections via sqlalchemy, and b) leaves the aiosqlite/sqlite connection in an unknown state when the thread is unceremoniously killed at program exit time. More importantly, with aiosqlite v0.22.0, the connection class is no longer a thread, meaning connection.daemon = True does nothing, and this results in the aiosqlite connection hanging the program while it waits for its connection to be closed (omnilib/aiosqlite#369).

Optional link from https://docs.sqlalchemy.org which documents the behavior that is expected

No response

SQLAlchemy Version in Use

main

DBAPI (i.e. the database driver)

aiosqlite 0.22.0

Database Vendor and Major Version

sqlite

Python Version

3.14

Operating system

macOS

To Reproduce

import asyncio

from sqlalchemy.ext.asyncio import create_async_engine

async def main():
    engine = create_async_engine("sqlite+aiosqlite:///:memory:")
    print("connecting")
    async with engine.begin() as conn:
        print("connected")

    print("done")


asyncio.run(main())

Error

connecting
connected
done
^CException ignored while joining a thread in _thread._shutdown():
Traceback (most recent call last):
  File "/Users/amethyst/.local/share/uv/python/cpython-3.14.0-macos-aarch64-none/lib/python3.14/threading.py", line 1582, in _shutdown
    _thread_shutdown()
KeyboardInterrupt:
Exception ignored while calling deallocator <function Connection.__del__ at 0x110545bc0>:
Traceback (most recent call last):
  File "/Users/amethyst/workspace/sqlalchemy/.venv/lib/python3.14/site-packages/aiosqlite/core.py", line 429, in __del__
    self._stop_running()
  File "/Users/amethyst/workspace/sqlalchemy/.venv/lib/python3.14/site-packages/aiosqlite/core.py", line 101, in _stop_running
    future = asyncio.get_event_loop().create_future()
  File "/Users/amethyst/.local/share/uv/python/cpython-3.14.0-macos-aarch64-none/lib/python3.14/asyncio/events.py", line 715, in get_event_loop
    raise RuntimeError('There is no current event loop in thread %r.'
RuntimeError: There is no current event loop in thread 'MainThread'.

Additional context

No response

Activity

  1. added
    engineengines, connections, transactions, isolation levels, execution options
    and removed
    requires triageNew issue that requires categorization
    on Dec 17, 2025
  2. CaselIT commented on Dec 17, 2025

    @CaselIT
    Member

    Hi,

    While the dialect will for sure need to be update to account for the change in aiosslite regarding the fact that the connection is no longer a thread, what you are seeing is kinda expected.

    By default sqlalchemy uses connection pooling with aiosqlite so it does not close the connection when async with engine.begin() as conn: exits; the connection is instead returned to the pool.

    So to fix this you can:

    • disable connection pooling, using NullPool instead
    • place a dispose call await engine.dispose() at the end of the main function so that all connections are correctly closed

    Overall the main issue is that python gc destructor call has no way of doing async io, so you need to properly manage the connection yourself

  3. added
    use casenot really a feature or a bug; can be support for new DB features or user use cases not anticipated
    on Dec 17, 2025
  4. added this to the 2.0.x milestone on Dec 17, 2025
  5. zzzeek commented on Dec 17, 2025

    @zzzeek
    Member

    I need to look at aiosqlite's new implementation (great news!) but we would also really benefit from having a non-async "terminate()" method of some kind so that when we detect that a connection has been thrown away (which we detect in a non-async weakref handler), we can ensure this connection is closed out without requiring the use of await.

  6. tseaver commented on Dec 17, 2025

    @tseaver

    FWIW, I was able to fix the hang in my app by adding an await engine.dispose() at the end of my app's lifetime function. That change seems correct to me: my code was previously skating with aiosqlite < 0.22.0.

  7. amyreese commented on Dec 17, 2025

    @amyreese
    Author

    I need to look at aiosqlite's new implementation (great news!) but we would also really benefit from having a non-async "terminate()" method of some kind so that when we detect that a connection has been thrown away (which we detect in a non-async weakref handler), we can ensure this connection is closed out without requiring the use of await.

    @zzzeek I've put up omnilib/aiosqlite#370 to promote the new stop mechanism to a supported API, which supports being called without an async context, but doesn't guarantee the background thread will be completed before stop() returns. If an async event loop is still available, stop() will return a future that can be awaited for that purpose.

    Would that be sufficient?

  8. zzzeek commented on Dec 17, 2025

    @zzzeek
    Member

    by the time we get to where we are calling "terminate()", there should not be any event loop for us, which means if stop() does a hard stop without needing await in that case, that would be sufficient. however it would be simpler if it did "stop without awaiting" in all cases becasue we wouldnt have to worry about a surprise awaitable there, but if that's not possible, not that big a deal.

    the terminate case is when things have already gone wrong further up the chain so it's expected to be non-graceful.

  9. CaselIT commented on Dec 18, 2025

    @CaselIT
    Member

    so on the sqlalchemy side we could:

    • check if the connection is a thread, and if not avoid deamon=True
    • check if the connection has a stop method and use that for terminate if available

    alternatively we do these based on the driver version

  10. CaselIT commented on Dec 22, 2025

    @CaselIT
    Member

    @zzzeek do you prefer if the updated logic described here #13039 (comment) is based on version number of on isinstance / hasattr?

  11. zzzeek commented on Dec 22, 2025

    @zzzeek
    Member

    id choose isinstance

  12. added a commit that references this issue on Dec 23, 2025
  13. 11 remaining items

  14. added a commit that references this issue on Jan 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    engineengines, connections, transactions, isolation levels, execution optionssqliteuse casenot really a feature or a bug; can be support for new DB features or user use cases not anticipated

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions