diff --git a/lib/sqlite3/database.rb b/lib/sqlite3/database.rb index 50b076e9..ded712dc 100644 --- a/lib/sqlite3/database.rb +++ b/lib/sqlite3/database.rb @@ -695,6 +695,25 @@ def readonly? @readonly end + # Sets a #busy_handler that releases the GVL between retries, + # but only retries up to the indicated number of +milliseconds+. + # This is an alternative to #busy_timeout, which holds the GVL + # while SQLite sleeps and retries. + def busy_handler_timeout=( milliseconds ) + timeout_seconds = milliseconds.fdiv(1000) + + busy_handler do |count| + now = Process.clock_gettime(Process::CLOCK_MONOTONIC) + if count.zero? + @timeout_deadline = now + timeout_seconds + elsif now > @timeout_deadline + next false + else + sleep(0.001) + end + end + end + # A helper class for dealing with custom functions (see #create_function, # #create_aggregate, and #create_aggregate_handler). It encapsulates the # opaque function object that represents the current invocation. It also diff --git a/test/test_integration_pending.rb b/test/test_integration_pending.rb index a230a2cb..6a391f53 100644 --- a/test/test_integration_pending.rb +++ b/test/test_integration_pending.rb @@ -74,4 +74,42 @@ def test_busy_timeout assert_operator time.real * 1000, :>=, 1000 end + + def test_busy_handler_timeout_releases_gvl + work = [] + + Thread.new do + while true + sleep 0.1 + work << '.' + end + end + sleep 1 + + @db.busy_handler_timeout = 1000 + busy = Mutex.new + busy.lock + + 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 1 + + assert_raises( SQLite3::BusyException ) do + work << '|' + @db.execute "insert into foo (b) values ( 'from 2' )" + end + + busy.unlock + t.join + + assert work.size - work.find_index('|') > 3 + end end