Repository navigation
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #889 +/- ##
==========================================
+ Coverage 95.41% 95.43% +0.02%
==========================================
Files 134 134
Lines 26802 26929 +127
==========================================
+ Hits 25574 25701 +127
Misses 1228 1228 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Codecov reported 90% patch coverage for PR #889 with 6 uncovered new lines in rope/contrib/autoimport/sqlite.py: - the guard branch in _register_connection() that closes the new connection and raises when the instance is already closed - the closed-instance guard in the `connection` setter - the setter path discarding the previously tracked thread-local connection when replaced by a different one Add tests hitting each of these paths so the patch coverage is complete.
|
I ran the lifetime tests from #888 against this branch (5756c12), plus two extra cases mcepl asked about. Results:
def test_connections_of_finished_threads_are_closed(project: Project):
with closing(AutoImport(project, memory=True)) as autoimport:
with ThreadPoolExecutor(1) as tp:
worker_connection = tp.submit(lambda: autoimport.connection).result()
# The next new connection closes the ones left by finished threads
with ThreadPoolExecutor(1) as tp:
tp.submit(lambda: autoimport.connection).result()
with pytest.raises(sqlite3.ProgrammingError, match="closed database"):
worker_connection.execute("SELECT 1")The retention also adds up: after 20 short-lived threads, each touching #888 handles this by keying the registry by One minor point: the Tested on Python 3.14, Windows. Once the exited-thread test (or an equivalent) is in this PR, I'm happy to close #888. |
|
I haven't tested this, but if avoiding the warning is all that's needed, I think we can simplify the implementation of this connection closing mechanism. If we put a ConnectionCloser object into the thread local, then when the thread is finished, it will garbage collect the thread_local, which would garbage collect the thread closer, which can close the sqlite connection: class ConnectionCloser:
def __init__(self, connection):
self.connection = connection
def __del__(self):
self.connection.close()@property
def connection(self):
"""
Creates a new connection if called from a new thread.
This makes sure AutoImport can be shared across threads.
"""
if not hasattr(self.thread_local, "connection"):
self.thread_local.connection = self.create_database_connection(
project=self.project,
memory=self.memory,
)
self.thread_local.closer = ConnectionCloser(self.thread_local.connection)
return self.thread_local.connectionThe connection closer should run on the same thread as the connection object, so it would not need to bypass And I think a global I'd really prefer not to have to bypass the Ideally, if worker threads don't want their connection objects to stay alive till the end of their thread, it should be that thread's responsibility to enter a context manager to manage the lifetime of their connection. My preference for AutoImport's concurrency model when the thread_local was originally implemented was that each worker threads should usually be creating their own instance of AutoImport. The use of thread local here means that each threads can have their own connection, but then they weren't required to manage the lifetime of their connection. I'll need to let this simmer in my head for a bit. |
|
I agree that each worker owning its own I am not sure a Would that approach address your concern, or would you prefer a different ownership model for these connections? |
5756c12 to
fb4883b
Compare
Codecov reported 90% patch coverage for PR #889 with 6 uncovered new lines in rope/contrib/autoimport/sqlite.py: - the guard branch in _register_connection() that closes the new connection and raises when the instance is already closed - the closed-instance guard in the `connection` setter - the setter path discarding the previously tracked thread-local connection when replaced by a different one Add tests hitting each of these paths so the patch coverage is complete.
AutoImport dynamically allocates per-thread SQLite connections using threading.local(). However, AutoImport.close() only closed the connection belonging to the calling thread, orphaning any connections created by worker threads and triggering ResourceWarning on Python 3.12+. Keep a thread-safe registry of all active connections on the AutoImport instance, create connections with check_same_thread=False, and ensure AutoImport.close() commits and closes all registered connections. Also expose close_thread_connection() for worker threads and implement context manager protocol support on AutoImport. Fixes: #886
Codecov reported 90% patch coverage for PR #889 with 6 uncovered new lines in rope/contrib/autoimport/sqlite.py: - the guard branch in _register_connection() that closes the new connection and raises when the instance is already closed - the closed-instance guard in the `connection` setter - the setter path discarding the previously tracked thread-local connection when replaced by a different one Add tests hitting each of these paths so the patch coverage is complete.
fb4883b to
1ea2f26
Compare
AutoImport dynamically allocates per-thread SQLite connections using threading.local(). However, AutoImport.close() only closed the connection belonging to the calling thread, orphaning any connections created by worker threads and triggering ResourceWarning on Python 3.12+.
Keep a thread-safe registry of all active connections on the AutoImport instance, create connections with check_same_thread=False, and ensure AutoImport.close() commits and closes all registered connections. Also expose close_thread_connection() for worker threads and implement context manager protocol support on AutoImport.
Fixes: #886
Checklist (delete if not relevant):