1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213
|
require "helper"
class IntegrationPendingTestCase < SQLite3::TestCase
class ThreadSynchronizer
def initialize
@main_to_thread = Queue.new
@thread_to_main = Queue.new
end
def send_to_thread state
@main_to_thread.push state
end
def send_to_main state
@thread_to_main.push state
end
def wait_for_thread expected_state, non_block = false
state = @thread_to_main.pop(non_block)
raise "Invalid state #{state}. #{expected_state} is expected" if state != expected_state
end
def wait_for_main expected_state, non_block = false
state = @main_to_thread.pop(non_block)
raise "Invalid state #{state}. #{expected_state} is expected" if state != expected_state
end
def close_thread
@thread_to_main.close
end
def close_main
@main_to_thread.close
end
def close
close_thread
close_main
end
end
def setup
@db = SQLite3::Database.new("test.db")
@db.transaction do
@db.execute "create table foo ( a integer primary key, b text )"
@db.execute "insert into foo ( b ) values ( 'foo' )"
@db.execute "insert into foo ( b ) values ( 'bar' )"
@db.execute "insert into foo ( b ) values ( 'baz' )"
end
end
def teardown
@db.close
File.delete("test.db")
end
def test_busy_handler_impatient
synchronizer = ThreadSynchronizer.new
handler_call_count = 0
t = Thread.new(synchronizer) do |sync|
db2 = SQLite3::Database.open("test.db")
db2.transaction(:exclusive) do
sync.send_to_main :ready_0
sync.wait_for_main :end_1
end
ensure
db2&.close
sync.close_thread
end
synchronizer.wait_for_thread :ready_0
@db.busy_handler do
handler_call_count += 1
false
end
assert_raise(SQLite3::BusyException) do
@db.execute "insert into foo (b) values ( 'from 2' )"
end
assert_equal 1, handler_call_count
synchronizer.send_to_thread :end_1
synchronizer.close_main
t.join
end
def test_busy_timeout
@db.busy_timeout 1000
synchronizer = ThreadSynchronizer.new
t = Thread.new(synchronizer) do |sync|
db2 = SQLite3::Database.open("test.db")
db2.transaction(:exclusive) do
sync.send_to_main :ready_0
sync.wait_for_main :end_1
end
ensure
db2&.close
sync.close_thread
end
synchronizer.wait_for_thread :ready_0
start_time = Time.now
assert_raise(SQLite3::BusyException) do
@db.execute "insert into foo (b) values ( 'from 2' )"
end
end_time = Time.now
assert_operator(end_time - start_time, :>=, 1.0)
synchronizer.send_to_thread :end_1
synchronizer.close_main
t.join
end
def test_busy_handler_timeout_releases_gvl
@db.busy_handler_timeout = 100
t1sync = ThreadSynchronizer.new
t2sync = ThreadSynchronizer.new
busy = Mutex.new
busy.lock
count = 0
active_thread = Thread.new(t1sync) do |sync|
sync.send_to_main :ready
sync.wait_for_main :start
loop do
sleep 0.005
count += 1
begin
sync.wait_for_main :end, true
break
rescue ThreadError
end
end
sync.send_to_main :done
end
blocking_thread = Thread.new(t2sync) do |sync|
db2 = SQLite3::Database.open("test.db")
db2.transaction(:exclusive) do
sync.send_to_main :ready
busy.lock
end
sync.send_to_main :done
ensure
db2&.close
end
t1sync.wait_for_thread :ready
t2sync.wait_for_thread :ready
t1sync.send_to_thread :start
assert_raises(SQLite3::BusyException) do
@db.execute "insert into foo (b) values ( 'from 2' )"
end
t1sync.send_to_thread :end
busy.unlock
t2sync.wait_for_thread :done
expected = if RUBY_PLATFORM.include?("linux")
# 20 is the theoretical max if timeout is 100ms and active thread sleeps 5ms
15
else
# in CI, macos and windows systems seem to really not thread very well, so let's set a lower bar.
2
end
assert_operator(count, :>=, expected)
ensure
active_thread&.join
blocking_thread&.join
t1sync&.close
t2sync&.close
end
def test_busy_handler_outwait
synchronizer = ThreadSynchronizer.new
handler_call_count = 0
t = Thread.new(synchronizer) do |sync|
db2 = SQLite3::Database.open("test.db")
db2.transaction(:exclusive) do
sync.send_to_main :ready_0
sync.wait_for_main :busy_handler_called_1
end
sync.send_to_main :end_of_transaction_2
ensure
db2&.close
sync.close_thread
end
synchronizer.wait_for_thread :ready_0
@db.busy_handler do |count|
handler_call_count += 1
synchronizer.send_to_thread :busy_handler_called_1
synchronizer.wait_for_thread :end_of_transaction_2
true
end
assert_nothing_raised do
@db.execute "insert into foo (b) values ( 'from 2' )"
end
assert_equal 1, handler_call_count
synchronizer.close_main
t.join
end
end
|