Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow the lifetime of the Connection thread to be tied to an event loop #112

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

puddly
Copy link

@puddly puddly commented Apr 21, 2021

Description

In projects where aiosqlite is used just as an async replacement for the sqlite3 module, it's not always possible to refactor downstream codebases to use async context managers and gracefully close aiosqlite. This results in undesirable behavior like Ctrl+C not immediately working, failing integration tests locking up test runners (how I found out GitHub Actions can run for six hours 😄), and the user eventually mashing Ctrl+C to kill the process anyways.

For the common use case with only a single event loop, tying the lifetime of the Connection object to this event loop with an optional keyword argument will allow aiosqlite to gracefully close the database connection without breaking any existing code. To increase responsiveness, you could even lower the queue polling timeout from 0.1 to 0.01.

Related issues:

@puddly
Copy link
Author

puddly commented Apr 21, 2021

The lint error with Python 3.7 and 3.8 is unrelated to this PR but can be fixed with # pylint: disable=too-many-ancestors or the appropriate pylint config:

[DESIGN]
max-parents=9

Copy link
Member

@amyreese amyreese left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the delay in getting to this. I'd mostly just like to keep the API and behavior simple and consistent, with less need to explicitly pass values to the connection to get the "right" behavior.

@@ -47,13 +47,15 @@ def __init__(
connector: Callable[[], sqlite3.Connection],
iter_chunk_size: int,
loop: Optional[asyncio.AbstractEventLoop] = None,
parent_loop: Optional[asyncio.AbstractEventLoop] = None,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason or benefit to explicitly passing the parent loop (something that was explicitly deprecated in previous versions), rather than just using get_loop to get the active loop when the connection object is made? Using get_loop instead will prevent errors if the non-active loop is passed in, and also provide more consistent behavior in the run() method.

I think I would also prefer tracking this as self._loop for brevity.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added it as an explicit parameter to preserve existing behavior and because get_loop may not immediately run, like if self._tx is never filled with any futures, and it would pick the first-seen event loop in the case of multiple loops, which may not be desired.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If get_loop is called within the _connect method, then there's never a chance that it would get the wrong event loop. I'd overall prefer to have consistent behavior with minimal parameters than have more parameters that result in a higher number of distinct codepaths.

@@ -116,6 +118,19 @@ def set_exception(fut, e):

get_loop(future).call_soon_threadsafe(set_exception, future, e)

# Clean up within this thread only if the parent event loop exits ungracefully
if not self._running or self._connection is None or self._parent_loop is None:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't obvious as to when it will return or run the cleanup below. Is there a way to always try cleaning up?

Comment on lines +480 to +481
with self.assertRaises(ValueError):
db.in_transaction
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe validate db._running also/instead?

@puddly
Copy link
Author

puddly commented May 17, 2021

Agreed. I'm still doing some more testing (specifically preventing IPython lockups when using aiosqlite within an interactive session) but maybe it's better to keep track of every observed event loop via get_loop?

Alternatively, maybe it's better to make the thread daemonic and possibly clean up when the thread is killed (if that is possible)? Or use a second daemon thread to signal the main thread to exit? Or atexit.register? I'm just thinking aloud, when I get some time I'll try these approaches out to see which work.

The "parent event loop" thing was just my original idea, I don't particularly care about the implementation details as long as the database is gracefully closed and the interpreter isn't stuck waiting for this thread when it exits.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants