Created
January 4, 2024 21:05
-
-
Save fractaledmind/8759a8f9409217afbd4b10498cb1638d to your computer and use it in GitHub Desktop.
This test script demonstrates how the busy_timeout holds the GVL while retrying, while a busy_handler timeout will release the GVL between retries
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'sqlite3' | |
require 'minitest/autorun' | |
puts "info: gem version: #{SQLite3::VERSION}" | |
puts "info: sqlite version: #{SQLite3::SQLITE_VERSION}/#{SQLite3::SQLITE_LOADED_VERSION}" | |
puts "info: sqlcipher?: #{SQLite3.sqlcipher?}" | |
puts "info: threadsafe?: #{SQLite3.threadsafe?}" | |
class TestCase < Minitest::Test | |
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_timeout_holds_gvl | |
# this is where we will store work done by our "working thread" | |
work = [] | |
# initialize the "working thread" which does work every tenth of a second | |
Thread.new do | |
while true | |
sleep 0.1 | |
work << '.' | |
end | |
end | |
# sleep for one second to ensure that our "working thread" has started running before proceeding | |
sleep 1 | |
# set the busy_timeout for our primary database connection | |
@db.busy_timeout 1000 | |
# initialize a mutex and lock it to give us sufficient control to ensure the busy exception is raised | |
busy = Mutex.new | |
busy.lock | |
# initialize the "busy thread" which keeps our database busy | |
t = Thread.new do | |
begin | |
db2 = SQLite3::Database.open( "test.db" ) | |
db2.transaction( :exclusive ) do | |
busy.lock | |
end | |
ensure | |
db2.close if db2 | |
end | |
end | |
# sleep for one second to ensure that our "busy thread" has started running before proceeding | |
sleep 1 | |
assert_raises( SQLite3::BusyException ) do | |
# mark where in the "work stream" we begin attempting to execute a SQL query against a busy database | |
work << '>' | |
@db.execute "insert into foo (b) values ( 'from 2' )" | |
end | |
# clean up the mutex and "busy thread" | |
busy.unlock | |
t.join | |
# busy_timeout only ever allows at most one more round of "work" from the "working thread" | |
# while attempting to connect to the database | |
p ['busy_handler_timeout', work] | |
assert 2 == work.size - work.find_index(">") | |
end | |
def test_busy_handler_timeout_releases_gvl | |
# this is where we will store work done by our "working thread" | |
work = [] | |
# initialize the "working thread" which does work every tenth of a second | |
Thread.new do | |
while true | |
sleep 0.1 | |
work << '.' | |
end | |
end | |
# sleep for one second to ensure that our "working thread" has started running before proceeding | |
sleep 1 | |
# set the busy_handler for our primary database connection, which is a GVL releasing timeout | |
@db.busy_handler do |count| | |
now = Process.clock_gettime(Process::CLOCK_MONOTONIC) | |
if count.zero? | |
@timeout_deadline = now + 1 | |
elsif now > @timeout_deadline | |
next false | |
else | |
sleep(0.001) | |
end | |
end | |
# initialize a mutex and lock it to give us sufficient control to ensure the busy exception is raised | |
busy = Mutex.new | |
busy.lock | |
# initialize the "busy thread" which keeps our database busy | |
t = Thread.new do | |
begin | |
db2 = SQLite3::Database.open( "test.db" ) | |
db2.transaction( :exclusive ) do | |
busy.lock | |
end | |
ensure | |
db2.close if db2 | |
end | |
end | |
# sleep for one second to ensure that our "busy thread" has started running before proceeding | |
sleep 1 | |
assert_raises( SQLite3::BusyException ) do | |
# mark where in the "work stream" we begin attempting to execute a SQL query against a busy database | |
work << '>' | |
@db.execute "insert into foo (b) values ( 'from 2' )" | |
end | |
# clean up the mutex and "busy thread" | |
busy.unlock | |
t.join | |
# busy_handler timeout allows many rounds of "work" from the "working thread" | |
# while attempting to connect to the database | |
p ['busy_handler_timeout', work] | |
assert 2 < work.size - work.find_index(">") | |
end | |
end |
That was approximately what my previous setup was. The tricky part is the test assertion. I was using a timing assertion, which was just flaky. I like my current assertion much more as I find it clearer. Does the work thread get to do work while the connecting thread is trying to connect.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
A simpler test for gvl could also look like this
One of these threads will throw a busy exception as it spinlocks trying to acquire the lock already held by the other thread, preventing the other thread from running and finishing the sleep statement. Only after it timeouts that the other thread can proceed, commit the transaction and release the write lock.