Conversation
A press during a long statement did nothing until the statement ended, because Python delivers a signal by setting a flag and raising it at the next bytecode on the main thread, and a thread inside the engine with the GIL released runs no bytecode. Ten seconds of that reads as a hang, and a notebook that looks hung gets its kernel killed. So a statement called from the main thread now runs on a thread the connection keeps, and the calling thread waits for it two milliseconds at a time, asking Python for signals in between. A press raises KeyboardInterrupt from execute at five milliseconds measured against the fifty the milestone asks for. Everywhere else the statement runs inline as it always did, because Python delivers a signal to the main thread and to no other, and a watch there would be a cost buying nothing. The thread is kept rather than made per statement, and made at all only at the first statement that needs it. Starting one costs thirty microseconds against a small statement that costs ten, and handing work to a parked thread costs six, so the thread looks for work for two hundred microseconds before it parks and the waiting side looks for the answer for as long before it sleeps. A statement that finishes on its own is not measurably slower for being watched, which is a test rather than a claim. interrupt() is the same stop from another thread, and raises Interrupted where the statement is. It takes no lock on the connection, because the engine handle it sets lives beside the lock rather than under it and an ask that queued behind the statement could only arrive after it. closed and rows_read are answerable while a statement runs for the same reason, which is what a progress bar is drawn from.
This was referenced Aug 18, 2026
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.
A press during a long statement did nothing until the statement ended. Python answers a signal by setting a flag in the handler and raising the exception at the next bytecode on the main thread, and a thread sitting inside the engine with the GIL released runs no bytecode, so a ten second query swallowed the press and gave it back ten seconds later. That is the shape of a hang, and a notebook that looks hung gets its kernel killed.
A statement called from the main thread now runs on a thread the connection keeps for it, while the calling thread waits two milliseconds at a time and asks Python for signals in between. A press raises
KeyboardInterruptout ofexecuteat five milliseconds measured on this machine, against the fifty the milestone asks for. A statement called from any other thread runs inline exactly as before, because Python delivers a signal to the main thread and to no other, and watching for one anywhere else is a cost that buys nothing.The thread is kept rather than made per statement, and it is only started at the first statement that needs it. Starting a thread costs thirty microseconds against a small statement that costs ten, and handing work to a parked one costs six, so the runner looks for work for two hundred microseconds before it parks and the waiting side looks for the answer for as long before it sleeps.
test_a_statement_that_finishes_is_not_slowed_by_being_watchedis the gate on that: a small statement on the main thread has to stay inside three times what it costs on a worker thread.interrupt()is the same stop asked for from another thread, and it raiseszudb.Interruptedwhere the statement is. It takes no lock on the connection: the engine handle it sets is held beside the lock rather than under it, so an ask cannot queue behind the statement it means to stop.closedandrows_readanswer while a statement runs for the same reason, androws_readis what a progress bar is drawn from.Eleven tests, all against a database of twelve thousand people whose pairwise statement runs about ten seconds: the press inside the budget, the ask inside the budget, a statement on a worker thread stopped too, the connection reading twelve thousand rows again afterwards, an ask with nothing running not ending the next statement, a press with nothing running still Python's to deliver, an interrupt on a closed connection saying so, a connection answering while it is busy, and a press during a statement that finished first still being raised.
Local gates:
ruff check,ruff format --check,cargo fmt --all --check,cargo clippy --all-targets -- -D warnings, andpytest, 246 passed.Part of DX2, tamnd/zu#168:
Ctrl-CraisesKeyboardInterruptin Python within 50 ms.