New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
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. |
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
end This, 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) |
Motivation / Background
SQLite's
busy_timeout
is a specific implementation of the lower-levelbusy_handler
function 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 thetimeout
number of milliseconds. Exposing access to this function via thetimeout
option in thedatabase.yml
file 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.yml
calledretries
, which is used in a simplebusy_handler
function 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]
timeout
option isn't tested, and testing either thebusy_timeout
orbusy_handler
function is difficult relative to its value