Allow SQLite3 busy_handler to be configured with simple max number of retries#49352
Conversation
…of `retries` Retrying busy connections without delay is a preferred practice for performance-sensitive applications. Add support for a `database.yml` `retries` integer, which is used in a simple `busy_handler` function to retry busy connections without exponential backoff up to the max number of `retries` .
|
@fractaledmind Where is the backoff implemented? That's new to me and I haven't been able to find the source or another reference to it. Thanks. |
|
@suwyn See |
|
Got it thanks. The default busy handler what I was looking for. Noting for reference that the current implementation of This |
|
|
|
Yes, can confirm the retries busy handler does not have the issue with the GIL. The challenge I have with For example a quick test locally with I really don't care about how many retries are needed though, what I care about is the latency. With a timeout I can be explicit as to how I want the system to behave in regards to busy errors and how long to wait before throwing them. Incidentally, in my rudimentary testing (simple rails app, apache benchmark), using the Timeout monkey patch but with a much smaller sleep (1 picosecond) was actually faster than the Retries handler. |
|
At Litestack, what we do is simply either sleep for a small amount of time (e.g. 100 microseconds) or just switch the current fiber in case of a fiber based environment. The retries approach as implemented might result in needing a very high number of retries in order to ensure no timing out very often, it will also result in executing many blocks of Ruby code from within C control frames, not the best recipe for performance sensitive situations. Maybe a simpler approach, that would still honor the timeout SLA and work for both a Fiber and Thread based environment would be to do the following:
some sample code db.busy_handler do |count|
timed_out = false
# capture the start time of this blocked write
@start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) if count == 0
# keep track of elapsed time every x iterations (to lower load)
if count % 100 == 0
@elapsed_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - @start_time
# fail if we exceed the timeout value (captured from the timeout config option, converted to seconds)
timed_out = @elapsed_time > @timeout
end
if timed_out
false # this will cause the BusyException to be raised
else
sleep 0.001 # sleep 1 millisecond (or whatever)
end
endThis, or something similar keeps the current familiar configuration and is easy to reason about and understand, while internally it allows for other threads/fibers to take control while the current context is blocked on a write lock |
That's what we have observed and why this retries implementation can actually be slower than sleeping. Also want to note that the lower level handler busy handler is much much (that's a scientific expression) faster than specifying a busy_handler here. Would be wonderful if underlining issue with the busy timeout could be solved. |
|
A C level handler will be naturally much faster than. Ruby one, but in order to allow other Ruby threads/fibers to run, you need to tap into the Ruby C API anyway and return control to the VM. I think the approach outlined above presents a good enough middle ground between fast-but-blocking and really-slow-but-concurrent. If you want to make it even more efficient, the forgo the time calculations and just bail out after a certain number of retries (while still sleeping a bit on every invocation) |
…hancedsqlite3-adapter Most of the enhancements provided by activerecord-enhancedsqlite3-adapter have been upstreamed into Rails, including: * Add support for generated columns in SQLite3 adapter rails/rails#49346 * Add SQLite3 support for supports_insert_returning? rails/rails#49290 * Allow overriding SQLite defaults from database.yml rails/rails#50460 * Performance tune the SQLite3 adapter connection configuration rails/rails#49349 * Allow SQLite3 busy_handler to be configured with simple max number of retries rails/rails#49352 One enhancement Joy still needs is a way to load SQLite3 extensions. There is a sqlpkg gem that would provide this, which includes generating an initializer that overrides the Rails SQLite3 adapter. Instead, I‘ve just adapted the enhancedsqlite3-adapter code and droppped into the lib/ directory. The code here also knows how to load an extension by gem name which is makes it useful for the sqlite_ulid gem currently in use.
…hancedsqlite3-adapter Most of the enhancements provided by activerecord-enhancedsqlite3-adapter have been upstreamed into Rails, including: * Add support for generated columns in SQLite3 adapter rails/rails#49346 * Add SQLite3 support for supports_insert_returning? rails/rails#49290 * Allow overriding SQLite defaults from database.yml rails/rails#50460 * Performance tune the SQLite3 adapter connection configuration rails/rails#49349 * Allow SQLite3 busy_handler to be configured with simple max number of retries rails/rails#49352 One enhancement Joy still needs is a way to load SQLite3 extensions. There is a sqlpkg gem that would provide this, which includes generating an initializer that overrides the Rails SQLite3 adapter. Instead, I‘ve just adapted the enhancedsqlite3-adapter code and droppped into the lib/ directory. The code here also knows how to load an extension by gem name which is makes it useful for the sqlite_ulid gem currently in use.
…hancedsqlite3-adapter Most of the enhancements provided by activerecord-enhancedsqlite3-adapter have been upstreamed into Rails, including: * Add support for generated columns in SQLite3 adapter rails/rails#49346 * Add SQLite3 support for supports_insert_returning? rails/rails#49290 * Allow overriding SQLite defaults from database.yml rails/rails#50460 * Performance tune the SQLite3 adapter connection configuration rails/rails#49349 * Allow SQLite3 busy_handler to be configured with simple max number of retries rails/rails#49352 One enhancement Joy still needs is a way to load SQLite3 extensions. There is a sqlpkg gem that would provide this, which includes generating an initializer that overrides the Rails SQLite3 adapter. Instead, I‘ve just adapted the enhancedsqlite3-adapter code and droppped into the lib/ directory. The code here also knows how to load an extension by gem name which is makes it useful for the sqlite_ulid gem currently in use.
Motivation / Background
SQLite's
busy_timeoutis a specific implementation of the lower-levelbusy_handlerfunction which simply retries busy connections with "exponential backoff" (not truly exponential, the backoff steps are [1, 2, 5, 10, 15, 20, 25, 25, 25, 50, 50, 100] ms and then 100 ms each step thereafter) up to thetimeoutnumber of milliseconds. Exposing access to this function via thetimeoutoption in thedatabase.ymlfile is useful, but insufficient.It is growingly common for production Rails applications to use SQLite as their database engine. A recommended performance optimization is to immediately retry busy connections, instead of waiting for backoffs.
Detail
This pull request adds support for a new option in the
database.ymlcalledretries, which is used in a simplebusy_handlerfunction to retry busy connections without exponential backoff up to the max number ofretries.Additional information
Checklist
Before submitting the PR make sure the following are checked:
[Fix #issue-number]timeoutoption isn't tested, and testing either thebusy_timeoutorbusy_handlerfunction is difficult relative to its value