-
Notifications
You must be signed in to change notification settings - Fork 200
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
Implement progress handler interface #458
base: main
Are you sure you want to change the base?
Conversation
+1 to adding the progress handler, maybe we need to make sure this doesn't explode if sqlite is omitting the progress handler in the compilation process |
… sqlite3_progress_handler function is present in the SQLite library
Previous discussion in sparklemotion#458 Storing `VALUE self` as context for `rb_sqlite3_busy_handler` is unsafe because as of Ruby 2.7, the GC compactor may move objects around which can lead to this reference pointing to either another random object or to garbage. Instead we can store the callback reference inside the malloced struct (`sqlite3Ruby`) which can't possibly move, and then inside the handler, get the callback reference from that struct. This however requires to define a mark function for the database object, and while I was at it, I implemented compaction support for it so we don't pin that proc.
Clearly didn't get it quite right with af1da42. Will study and try again. |
You should start by rebasing on my PR, you'd have the new functions lined up etc. |
I had a stray line in the |
Ah, I didn't notice there was an issue with TruffleRuby, seems pretty easy to fix though:
Need to test for that function. I'll also report it to Truffle, as it should be trivial for them to add it. |
Previous discussion in sparklemotion#458 Storing `VALUE self` as context for `rb_sqlite3_busy_handler` is unsafe because as of Ruby 2.7, the GC compactor may move objects around which can lead to this reference pointing to either another random object or to garbage. Instead we can store the callback reference inside the malloced struct (`sqlite3Ruby`) which can't possibly move, and then inside the handler, get the callback reference from that struct. This however requires to define a mark function for the database object, and while I was at it, I implemented compaction support for it so we don't pin that proc.
All green. Thanks so much for the guidance @byroot. |
Never seen this error before, but it looks to be completely unrelated to this PR (from
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, I'm no owner of this gem, and my interest for sqlite3 is rather limited, but if the goal is to implement a statement timeout, I'm not offering a Proc interface is the best.
To call back into Ruby you need to hold the GVL. Right now it's not a problem because the gem calls sqlite3_step
with the GVL held. However there is demand for releasing the GVL when executing a query. I don't know if it will happen, but such API would render such change impossible.
If the main envisioned usage is a statement timeout, then it's probably better to implement it all in C so the overhead is much smaller and that it doesn't need to hold the GVL.
My thought was to provide layers of exposure. The However, when we turn to the PR for the higher-level |
Another great use-case for a raw |
@tenderlove @flavorjones: I believe this is ready to go. |
Just realized that in addition to switching fibers, this also unlocks the ability for apps to switch threads while queries are executing 👀 |
@flavorjones @tenderlove: Merged changes from Also, just to add some additional info, Simon Willison of Datasette did some research on approximately how long it takes for SQLite to execute 1,000 VM steps (simonw/datasette#1679 (comment)) and the approximate answer is 0.1ms. |
This is going to be a really nice feature when we merge it. I've been experimenting with using this to support better CPU saturation in multi-threaded environments and the results are quite nice. |
@fractaledmind you might want to look at how this is done in Extralite. There are some nuances that are important to keep in mind. The basic problem is that the SQLite VM instruction counter is reset for each query, so depending on the // register progress handler and invoke it every 10 VM instructions
sqlite3_progress_handler(db, 10, my_progress_handler, NULL);
// running the following query repeatedly will never invoke the progress handler
while (true) sqlite3_exec(db, "select 1", NULL, NULL, NULL); On the other hand, if you set a So what I did was to set the // from the Extralite code:
int Database_progress_handler(void *ptr) {
Database_t *db = (Database_t *)ptr;
db->progress_handler.tick_count += db->progress_handler.tick;
if (db->progress_handler.tick_count < db->progress_handler.period)
goto done;
db->progress_handler.tick_count -= db->progress_handler.period;
rb_funcall(db->progress_handler.proc, ID_call, 0);
done:
return 0;
} Also, note that the progress handler above always returns 0, which tells SQLite that the query can continue. Interrupting a query from the progress handler can be done by raising an exception, or by calling |
This adds support for the
progress_handler
SQLite interface. Such a callback is useful as it allows for statement timeouts to be defined. For example, we could define astatement_timeout=
method on the database instance like so:Note: I don't know C, so this PR was written using pattern matching (primarily on the
rb_sqlite3_busy_handler
andbusy_handler
methods), some Googling (for how to get the correct format forrb_scan_args
), and ChatGPT (to explain what all of this C code was doing).