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