diff --git a/ext/sqlite3/database.c b/ext/sqlite3/database.c index 2c949b30..cf40e617 100644 --- a/ext/sqlite3/database.c +++ b/ext/sqlite3/database.c @@ -524,24 +524,46 @@ set_sqlite3_func_result(sqlite3_context *ctx, VALUE result) } } -static void -rb_sqlite3_func(sqlite3_context *ctx, int argc, sqlite3_value **argv) +typedef struct { + sqlite3_context *ctx; + int argc; + sqlite3_value **argv; +} rb_sqlite3_func_args_t; + +static VALUE +rb_sqlite3_func_protected(VALUE func_args_value) { - VALUE callable = (VALUE)sqlite3_user_data(ctx); - VALUE params = rb_ary_new2(argc); + rb_sqlite3_func_args_t *args = (rb_sqlite3_func_args_t *)func_args_value; + VALUE callable = (VALUE)sqlite3_user_data(args->ctx); + VALUE params = rb_ary_new2(args->argc); VALUE result; int i; - if (argc > 0) { - for (i = 0; i < argc; i++) { - VALUE param = sqlite3val2rb(argv[i]); - rb_ary_push(params, param); - } + for (i = 0; i < args->argc; i++) { + VALUE param = sqlite3val2rb(args->argv[i]); + rb_ary_push(params, param); } result = rb_apply(callable, rb_intern("call"), params); - set_sqlite3_func_result(ctx, result); + set_sqlite3_func_result(args->ctx, result); + + return Qnil; +} + +static void +rb_sqlite3_func(sqlite3_context *ctx, int argc, sqlite3_value **argv) +{ + rb_sqlite3_func_args_t args = { .ctx = ctx, .argc = argc, .argv = argv }; + int exc_status; + + rb_protect(rb_sqlite3_func_protected, (VALUE)&args, &exc_status); + + if (exc_status) { + /* the user should never see this message, because Statement#step will + * re-raise the exception still in rb_errinfo */ + sqlite3_result_error(ctx, "Ruby Exception occurred", -1); + } } #ifndef HAVE_RB_PROC_ARITY diff --git a/test/test_database.rb b/test/test_database.rb index 4b02ee6e..05055559 100644 --- a/test/test_database.rb +++ b/test/test_database.rb @@ -520,6 +520,45 @@ def test_function_return_types end end + def test_function_raise_propagates_and_connection_remains_usable + @db.define_function("boom") { |a| raise "boom: #{a}" } + + error = assert_raise(RuntimeError) { @db.execute("select boom(1)") } + assert_equal("boom: 1", error.message) + + assert_equal([[2]], @db.execute("select 1 + 1")) + end + + def test_function_raise_does_not_deadlock_other_threads_using_the_connection + skip("interpreter doesn't support fork") unless Process.respond_to?(:fork) + skip("valgrind doesn't handle forking") if i_am_running_in_valgrind + + @db.close + read, write = IO.pipe + old_stderr, $stderr = $stderr, StringIO.new + pid = Process.fork do + read.close + db = SQLite3::Database.new(":memory:") + db.define_function("boom") { |a| raise "boom" } + begin + db.execute("select boom(1)") + rescue RuntimeError + end + Thread.new { db.execute("select 1") }.join + write.write("ok") + exit! + end + $stderr = old_stderr + write.close + + result = IO.select([read], nil, nil, 10) && read.gets + Process.kill(:KILL, pid) unless result + Process.waitpid(pid) + read.close + + assert_equal("ok", result, "second thread deadlocked on the connection after a function raised") + end + def test_function_gc_segfault @db.create_function("bug", -1) { |func, *values| func.result = values.join } # With a lot of data and a lot of threads, try to induce a GC segfault.