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

Initial Ractor support #365

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions ext/sqlite3/extconf.rb
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ def configure_extension
# Functions defined in 2.1 but not 2.0
have_func("rb_integer_pack")

# Functions defined in 3.0 but not 2.7
have_func("rb_ext_ractor_safe")

# These functions may not be defined
have_func("sqlite3_initialize")
have_func("sqlite3_backup_init")
Expand Down
6 changes: 6 additions & 0 deletions ext/sqlite3/sqlite3.c
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ init_sqlite3_constants(void)
VALUE mSqlite3Constants;
VALUE mSqlite3Open;

#ifdef HAVE_RB_EXT_RACTOR_SAFE
if (sqlite3_threadsafe()) {
rb_ext_ractor_safe(true);
}
#endif

mSqlite3Constants = rb_define_module_under(mSqlite3, "Constants");

/* sqlite3_open_v2 flags for Database::new */
Expand Down
5 changes: 5 additions & 0 deletions lib/sqlite3.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,9 @@ module SQLite3
def self.threadsafe?
threadsafe > 0
end

# Is the gem's C extension marked as Ractor-safe?
def self.ractor_safe?
threadsafe? && !defined?(Ractor).nil?
end
end
1 change: 1 addition & 0 deletions sqlite3.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ Gem::Specification.new do |s|
"test/test_integration_aggregate.rb",
"test/test_integration_open_close.rb",
"test/test_integration_pending.rb",
"test/test_integration_ractor.rb",
"test/test_integration_resultset.rb",
"test/test_integration_statement.rb",
"test/test_pragmas.rb",
Expand Down
1 change: 1 addition & 0 deletions test/helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
puts "info: sqlite version: #{SQLite3::SQLITE_VERSION}/#{SQLite3::SQLITE_LOADED_VERSION}"
puts "info: sqlcipher?: #{SQLite3.sqlcipher?}"
puts "info: threadsafe?: #{SQLite3.threadsafe?}"
puts "info: ractor_safe?: #{SQLite3.ractor_safe?}"

module SQLite3
class TestCase < Minitest::Test
Expand Down
80 changes: 80 additions & 0 deletions test/test_integration_ractor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# frozen_string_literal: true

require "helper"
require "fileutils"

class IntegrationRactorTestCase < SQLite3::TestCase
STRESS_DB_NAME = "stress.db"

def setup
teardown
end

def teardown
FileUtils.rm_rf(Dir.glob("#{STRESS_DB_NAME}*"))
end

def test_ractor_safe
skip unless RUBY_VERSION >= "3.0" && SQLite3.threadsafe?
assert_predicate SQLite3, :ractor_safe?
end

def test_ractor_share_database
skip("Requires Ruby with Ractors") unless SQLite3.ractor_safe?

db = SQLite3::Database.open(":memory:")

if RUBY_VERSION >= "3.3"
# after ruby/ruby@ce47ee00
ractor = Ractor.new do
Ractor.receive
end

assert_raises(Ractor::Error) { ractor.send(db) }
else
# before ruby/ruby@ce47ee00 T_DATA objects could be copied
ractor = Ractor.new do
local_db = Ractor.receive
Ractor.yield local_db.object_id
end
ractor.send(db)
copy_id = ractor.take

assert_not_equal db.object_id, copy_id
end
end

def test_ractor_stress
skip("Requires Ruby with Ractors") unless SQLite3.ractor_safe?

# Testing with a file instead of :memory: since it can be more realistic
# compared with real production use, and so discover problems that in-
# memory testing won't find. Trivial example: STRESS_DB_NAME needs to be
# frozen to pass into the Ractor, but :memory: might avoid that problem by
# using a literal string.
db = SQLite3::Database.open(STRESS_DB_NAME)
db.execute("PRAGMA journal_mode=WAL") # A little slow without this
db.execute("create table stress_test (a integer primary_key, b text)")
random = Random.new.freeze
ractors = (0..9).map do |ractor_number|
Ractor.new(random, ractor_number) do |random, ractor_number|
db_in_ractor = SQLite3::Database.open(STRESS_DB_NAME)
db_in_ractor.busy_handler do
sleep random.rand / 100 # Lots of busy errors happen with multiple concurrent writers
true
end
100.times do |i|
db_in_ractor.execute("insert into stress_test(a, b) values (#{ractor_number * 100 + i}, '#{random.rand}')")
end
end
end
ractors.each { |r| r.take }
final_check = Ractor.new do
db_in_ractor = SQLite3::Database.open(STRESS_DB_NAME)
res = db_in_ractor.execute("select count(*) from stress_test")
Ractor.yield res
end
res = final_check.take
assert_equal 1000, res[0][0]
end
end