Skip to content

Commit 10e89a1

Browse files
committed
Initial Ractor support
1 parent cc3fb2e commit 10e89a1

File tree

4 files changed

+100
-1
lines changed

4 files changed

+100
-1
lines changed

ext/sqlite3/extconf.rb

+3
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,9 @@ def configure_extension
108108
# Functions defined in 2.1 but not 2.0
109109
have_func('rb_integer_pack')
110110

111+
# Functions defined in 3.0 but not 2.7
112+
have_func('rb_ext_ractor_safe')
113+
111114
# These functions may not be defined
112115
have_func('sqlite3_initialize')
113116
have_func('sqlite3_backup_init')

ext/sqlite3/sqlite3.c

+5
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ static VALUE threadsafe_p(VALUE UNUSED(klass))
8484

8585
void init_sqlite3_constants()
8686
{
87+
#ifdef HAVE_RB_EXT_RACTOR_SAFE
88+
if (sqlite3_threadsafe()) {
89+
rb_ext_ractor_safe(true);
90+
}
91+
#endif
8792
VALUE mSqlite3Constants;
8893
VALUE mSqlite3Open;
8994

lib/sqlite3/database.rb

+8-1
Original file line numberDiff line numberDiff line change
@@ -724,7 +724,14 @@ def translate_from_db types, row
724724

725725
private
726726

727-
NULL_TRANSLATOR = lambda { |_, row| row }
727+
# NULL_TRANSLATOR used to be a lambda, but a lambda can't be frozen (properly)
728+
# and so can't work with ractors.
729+
class NullTranslatorImplementation
730+
def self.call(_, row)
731+
row
732+
end
733+
end
734+
NULL_TRANSLATOR = NullTranslatorImplementation
728735

729736
def make_type_translator should_translate
730737
if should_translate

test/test_integration_ractor.rb

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# frozen_string_literal: true
2+
3+
require 'helper'
4+
require 'fileutils'
5+
6+
class TC_Integration_Ractor < SQLite3::TestCase
7+
STRESS_DB_NAME = "stress.db"
8+
9+
def setup
10+
teardown
11+
end
12+
13+
def teardown
14+
FileUtils.rm_rf(Dir.glob "#{STRESS_DB_NAME}*")
15+
end
16+
17+
def test_ractor_share_database
18+
skip('Requires Ruby with Ractors') if RUBY_VERSION < '3.2.0'
19+
assert SQLite3.threadsafe?
20+
21+
db_receiver = Ractor.new do
22+
db = Ractor.receive
23+
Ractor.yield db.object_id
24+
begin
25+
db.execute("create table test_table ( b integer primary key)")
26+
raise "Should have raised an exception in db.execute()"
27+
rescue => e
28+
Ractor.yield e
29+
end
30+
end
31+
db_creator = Ractor.new(db_receiver) do |db_receiver|
32+
db = SQLite3::Database.open(":memory:")
33+
Ractor.yield db.object_id
34+
db_receiver.send(db)
35+
sleep 0.1
36+
db.execute("create table test_table ( a integer primary key)")
37+
end
38+
first_oid = db_creator.take
39+
second_oid = db_receiver.take
40+
assert_not_equal first_oid, second_oid
41+
ex = db_receiver.take
42+
# For now, let's assert that you can't pass database connections around
43+
# between different Ractors. Letting a live DB connection exist in two
44+
# threads that are running concurrently might expose us to footguns and
45+
# lead to data corruption, so we should avoid this possibility and wait
46+
# until connections can be given away using `yield` or `send`.
47+
assert_equal "prepare called on a closed database", ex.message
48+
end
49+
50+
def test_ractor_stress
51+
skip('Requires Ruby with Ractors') if RUBY_VERSION < '3.2.0'
52+
assert SQLite3.threadsafe?
53+
54+
# Testing with a file instead of :memory: since it can be more realistic
55+
# compared with real production use, and so discover problems that in-
56+
# memory testing won't find. Trivial example: STRESS_DB_NAME needs to be
57+
# frozen to pass into the Ractor, but :memory: might avoid that problem by
58+
# using a literal string.
59+
db = SQLite3::Database.open(STRESS_DB_NAME)
60+
db.execute("PRAGMA journal_mode=WAL") # A little slow without this
61+
db.execute("create table stress_test (a integer primary_key, b text)")
62+
random = Random.new.freeze
63+
ractors = (0..9).map do |ractor_number|
64+
Ractor.new(random, ractor_number) do |random, ractor_number|
65+
db_in_ractor = SQLite3::Database.open(STRESS_DB_NAME)
66+
db_in_ractor.busy_handler do
67+
sleep random.rand / 100 # Lots of busy errors happen with multiple concurrent writers
68+
true
69+
end
70+
100.times do |i|
71+
db_in_ractor.execute("insert into stress_test(a, b) values (#{ractor_number * 100 + i}, '#{random.rand}')")
72+
end
73+
end
74+
end
75+
ractors.each {|r| r.take}
76+
final_check = Ractor.new do
77+
db_in_ractor = SQLite3::Database.open(STRESS_DB_NAME)
78+
res = db_in_ractor.execute("select count(*) from stress_test")
79+
Ractor.yield res
80+
end
81+
res = final_check.take
82+
assert_equal 1000, res[0][0]
83+
end
84+
end

0 commit comments

Comments
 (0)