Last month I released version 0.4.0 of the activerecord-enhancedsqlite3-adapter
gem, which added support for a non-blocking busy_timeout
to the SQLite adapter in Rails. As with all of my lower-level SQLite work, the goal is always to push these solutions into the foundations of the ecosystem so that everyone can benefit from them. In this case, I’m happy that the busy_timeout
feature has made its way into the sqlite3
gem, which is the most popular SQLite gem for Ruby.
I wrote last month about why SQLite needs a non-blocking busy_timeout
to improve concurrency. The problem with the sqlite3
gem lies in the interaction between SQLite’s C code and Ruby code. While SQLite is running the busy_timeout
function in its C code, our Ruby code that triggered the busy_timeout
function is still running, and so still holding the lock to Ruby’s GVL (Global VM Lock). That means that no other Ruby code can run while SQLite is running the busy_timeout
function. This negates the concurrency coordination that the busy_timeout
function is trying to achieve.
Since changing the nature of the interaction between SQLite’s C code and Ruby code is a major undertaking, I wanted to find a way to solve this problem without changing the nature of the interaction. I was able to do this by implementing a busy_handler
in Ruby that respects a timeout value but also releases the GVL between retry attempts. This allows other Ruby code to run while SQLite is waiting for the connection.
You can read the full details of the implementation in the pull request that added the feature to the sqlite3
gem. But here is the method itself:
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 endend
The idea, as you might have guessed, is to offer an alternative to the busy_timeout
which utilizes a custom busy_handler
. Thus, the new method is named busy_handler_timeout
—creative, right?
This feature is now in main
, but a new feature of the sqlite3
gem has not yet been released (as of the time I am writing this). I will definitely let you know which version of the sqlite3
gem includes this feature. In the meantime, you can use the activerecord-enhancedsqlite3-adapter
gem to get this feature in your Rails app.
The next step is to use this feature in Rails. Once a new version of the sqlite3
gem is released, I’ll open a pull request that wires up the timeout
value in the config/database.yml
file to this new busy_handler_timeout
feature. This will allow you to set a timeout value in your Rails app as you have always done, but connection retries won’t block concurrent threads.
I’m happy to see this feature make its way into the sqlite3
gem, and I’m looking forward to seeing it in Rails soon. I hope you find it useful in your apps!