Conversation
zudb.aio gives every connection a thread of its own and hands it every call that could wait. A statement is inside Rust with the GIL down, so one run straight from a coroutine stops the loop for as long as it takes, including the tasks answering requests that have nothing to do with the database. A thread per connection rather than a pool shared between them, because a connection is one lock and statements on it queue anyway. A pool would add no parallelism the engine can use and would let two statements written one after the other run in the other order. Two connections do run at the same time, and two statements together cost 1.09 times what one costs alone. What comes back is a zudb.Result, which is rows already in memory, so reading them is not awaited and never was. Only the ways in are, and only the ones that can wait: path, read_only, closed, rows_read and interrupt stay properties, answered from beside the lock, so a progress bar still reads while the statement it measures runs. Cancelling the task that awaits a statement interrupts the statement and waits for it to stop, so the connection is idle by the time the CancelledError arrives rather than busy with work nobody wants. A statement still queued is dropped without running, and a transaction block cancelled partway leaves through the rollback. The module is a submodule to ask for by name, not one the package imports, so a script that never awaits anything pays for neither asyncio nor the thread pool, and there is a test that says so.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
zudb.aiois the client on an event loop. A statement runs inside Rust with the GIL down and comes back when it comes back, so one called straight from a coroutine stops the loop for that whole time, including the tasks answering requests that have nothing to do with the database. Each connection here gets a thread of its own and every call that could wait is handed to it and awaited.A thread per connection
Rather than a pool shared between them, because a connection is one lock and statements on it queue anyway. A pool would add no parallelism the engine can use and would let two statements written one after the other run in the other order. Two connections do run at the same time, since the engine puts the GIL down for the work, and two statements together cost 1.09 times what one costs alone on this machine.
What is awaited and what is not
What comes back is a
zudb.Result, which is rows already in memory, so reading them is the call it was:for row in rowsandrows.to_arrow()are not awaited. Only the ways in are, and only the ones that can wait.path,read_only,closed,rows_readandinterrupt()are answered from beside the lock rather than through it, so they stay properties and a progress bar drawn fromrows_readstill reads while the statement it measures runs.in_transaction()andregistered()are methods where the sync client has properties, because both answers live behind the lock and a property that gave back a coroutine would read asif conn.in_transaction:, which is true whatever the answer.connect(),transaction()andappender()all reach the engine, so all three are coroutines, and a coroutine is not a context manager. Each returns something that is both: awaiting it opens the thing andasync withopens it and enters it, so the two spellings a caller might reach for are the same call.Cancellation
Cancelling the task that awaits a statement interrupts the statement. The engine is asked to stop and the coroutine does not return until it has, so the connection is idle again by the time the
CancelledErrorreaches the caller rather than busy with work nobody is waiting for. Which statement gets stopped is guarded, so a cancellation either interrupts its own statement or interrupts nothing, and never lands on the one after it. A statement still queued when the cancellation arrives is dropped without running. A transaction block cancelled partway is a block that raised, so it leaves through the rollback.Not imported until asked for
The module pulls in asyncio and a thread pool, and a script that never awaits anything should pay for neither, so the package does not import it.
import zudbis still inside its budget and there is now a test that says asyncio is not insys.modulesafterwards.Tests
25 in
tests/test_aio.py, run withasyncio.runsince the suite has no async plugin. The three claims are each checked directly: the loop ticks more than twenty times through one statement, statements arrive in the order they were awaited, and the statement after a cancelled one starts at once rather than queueing behind the work that was stopped.The long statement runs over a loaded throng rather than a hundred and fifty inserted people, because in a release build the smaller one is over in eleven milliseconds and there is nothing left to cancel.
The README gains a section, and it is a section that runs: the rule for which blocks are whole programs now reads "starts with an import" rather than "starts with
import zudb", so the async example is executed like the other two.Local gate is green: ruff check, ruff format, and the full pytest suite in a release build.