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

Implement progress handler interface #458

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6c4e155
Add the progress_handler interface
fractaledmind Jan 7, 2024
046bd67
Add tests for the progress_handler interface
fractaledmind Jan 7, 2024
11a94dc
Merge branch 'main' into progress-handler
fractaledmind Jan 7, 2024
50f1ef0
Remove extra line
fractaledmind Jan 7, 2024
c6bf834
Bump the frequency for the opcode arg test
fractaledmind Jan 7, 2024
3681b2a
Bump the frequency for the opcode arg test and make assertion more re…
fractaledmind Jan 7, 2024
c0d5144
Only define the ruby progress_handler method if the OMIT compilation …
fractaledmind Jan 7, 2024
02e6f0a
Properly protect the progress_handler depending on whether or not the…
fractaledmind Jan 9, 2024
da216c9
Skip progress_handler tests if the method is not defined
fractaledmind Jan 9, 2024
35a003a
WIP: make progress_handler GC-compaction safe
fractaledmind Jan 9, 2024
af1da42
Improve the compaction-safe version of the progress_handler
fractaledmind Jan 9, 2024
ce03826
Remove allocation bit
fractaledmind Jan 9, 2024
2aa7eae
Merge remote-tracking branch 'upstream/main' into progress-handler
fractaledmind Jan 9, 2024
22679e4
Merge branch 'main' into progress-handler
fractaledmind Jan 10, 2024
824627d
Merge branch 'main' into progress-handler
fractaledmind Jan 18, 2024
535d184
Default to progress handler every 1000 VM instructions
fractaledmind Jan 18, 2024
e12ef7d
Merge branch 'main' into progress-handler
fractaledmind Jan 26, 2024
4eb2870
Fix conditional check for defining progress_handler method
fractaledmind Jan 26, 2024
e7debf0
Add test showing how progress_handler can be used to increase concurr…
fractaledmind Jan 26, 2024
0fdd551
Fix Standard issue
fractaledmind Jan 26, 2024
dcd3b08
Fix tests now that the default number of vm steps for the progress_ha…
fractaledmind Jan 26, 2024
e5a18b9
Loosen test assertion for progress_handler releasing GVL
fractaledmind Jan 26, 2024
361a7e2
Loosen test assertion for progress_handler releasing GVL even more
fractaledmind Jan 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions ext/sqlite3/database.c
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ database_mark(void *ctx)
{
sqlite3RubyPtr c = (sqlite3RubyPtr)ctx;
rb_gc_mark(c->busy_handler);
rb_gc_mark(c->progress_handler);
}

static void
Expand Down Expand Up @@ -51,6 +52,7 @@ static VALUE
allocate(VALUE klass)
{
sqlite3RubyPtr ctx;

return TypedData_Make_Struct(klass, sqlite3Ruby, &database_type, ctx);
}

Expand Down Expand Up @@ -259,6 +261,57 @@ busy_handler(int argc, VALUE *argv, VALUE self)
return self;
}

#ifdef HAVE_SQLITE3_PROGRESS_HANDLER
static int
rb_sqlite3_progress_handler(void *context)
{
sqlite3RubyPtr ctx = (sqlite3RubyPtr)context;

VALUE handle = ctx->progress_handler;
VALUE result = rb_funcall(handle, rb_intern("call"), 0);

if (Qfalse == result) { return 1; }

return 0;
}

/* call-seq:
* progress_handler([n]) { ... }
* progress_handler([n,] Class.new { def call; end }.new)
*
* Register a progress handler with this database instance.
* This handler will be invoked periodically during a long-running query or operation.
* If the handler returns +false+, the operation will be interrupted; otherwise, it continues.
* The parameter 'n' specifies the number of SQLite virtual machine instructions between invocations.
* If 'n' is not provided, the default value is 1.
*/
static VALUE
progress_handler(int argc, VALUE *argv, VALUE self)
{
sqlite3RubyPtr ctx;
VALUE block, n_value;

TypedData_Get_Struct(self, sqlite3Ruby, &database_type, ctx);
REQUIRE_OPEN_DB(ctx);

rb_scan_args(argc, argv, "02", &n_value, &block);

int n = NIL_P(n_value) ? 1000 : NUM2INT(n_value);
if (NIL_P(block) && rb_block_given_p()) { block = rb_block_proc(); }
ctx->progress_handler = block;

sqlite3_progress_handler(
ctx->db,
n,
NIL_P(block) ? NULL : rb_sqlite3_progress_handler,
(void *)ctx
);

return self;
}
#endif


/* call-seq: last_insert_row_id
*
* Obtains the unique row ID of the last row to be inserted by this Database
Expand Down Expand Up @@ -888,6 +941,10 @@ init_sqlite3_database(void)
rb_define_method(cSqlite3Database, "enable_load_extension", enable_load_extension, 1);
#endif

#ifdef HAVE_SQLITE3_PROGRESS_HANDLER
rb_define_method(cSqlite3Database, "progress_handler", progress_handler, -1);
#endif

rb_sqlite3_aggregator_init();
}

Expand Down
1 change: 1 addition & 0 deletions ext/sqlite3/database.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
struct _sqlite3Ruby {
sqlite3 *db;
VALUE busy_handler;
VALUE progress_handler;
};

typedef struct _sqlite3Ruby sqlite3Ruby;
Expand Down
1 change: 1 addition & 0 deletions ext/sqlite3/extconf.rb
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ def configure_extension
have_func("sqlite3_column_database_name")
have_func("sqlite3_enable_load_extension")
have_func("sqlite3_load_extension")
have_func("sqlite3_progress_handler")

unless have_func("sqlite3_open_v2") # https://www.sqlite.org/releaselog/3_5_0.html
abort("\nPlease use a version of SQLite3 >= 3.5.0\n\n")
Expand Down
1 change: 1 addition & 0 deletions lib/sqlite3/database.rb
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ def initialize file, options = {}, zvfs = nil
@tracefunc = nil
@authorizer = nil
@busy_handler = nil
@progress_handler = nil
@collations = {}
@functions = {}
@results_as_hash = options[:results_as_hash]
Expand Down
62 changes: 62 additions & 0 deletions test/test_integration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -505,4 +505,66 @@ def test_bind_array_parameter
[1, "foo"])
assert_equal "foo", result
end

###
# The `progress_handler` method may not exist depending on how sqlite3 was compiled
def test_progress_handler_used
skip("progress_handler method not defined") unless @db.respond_to?(:progress_handler)

progress_calls = []
@db.progress_handler(10) do
progress_calls << nil
true
end
@db.execute "create table test1(a, b)"

assert_operator 1, :<, progress_calls.size
end

def test_progress_handler_opcode_arg
skip("progress_handler method not defined") unless @db.respond_to?(:progress_handler)

progress_calls = []
handler = proc do
progress_calls << nil
true
end
@db.progress_handler(1, handler)
@db.execute "create table test1(a, b)"
first_count = progress_calls.size

progress_calls = []
@db.progress_handler(10, handler)
@db.execute "create table test2(a, b)"
second_count = progress_calls.size

assert_operator first_count, :>=, second_count
end

def test_progress_handler_interrupts_operation
skip("progress_handler method not defined") unless @db.respond_to?(:progress_handler)

@db.progress_handler(10) do
false
end

assert_raises(SQLite3::InterruptException) do
@db.execute "create table test1(a, b)"
end
end

def test_clear_handler
skip("progress_handler method not defined") unless @db.respond_to?(:progress_handler)

progress_calls = []
@db.progress_handler do
progress_calls << nil
true
end
@db.progress_handler(nil)

@db.execute "create table test1(a, b)"

assert_equal 0, progress_calls.size
end
end
27 changes: 27 additions & 0 deletions test/test_integration_pending.rb
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,31 @@ def test_busy_handler_timeout_releases_gvl

assert_operator work.size - work.find_index("|"), :>, 3
end

def test_progress_handler_releasing_gvl
work = []

Thread.new do
loop do
sleep 0.1
work << "."
end
end

@db.progress_handler { Thread.pass }

work << ">"
@db.execute <<~SQL
WITH RECURSIVE r(i) AS (
VALUES(0)
UNION ALL
SELECT i FROM r
LIMIT 10000000
)
SELECT i FROM r WHERE i = 1;
SQL
work << "<"

assert_operator work.find_index("<") - work.find_index(">"), :>, 2
end
end