activerecord-dynamic_timeout
Dynamic query timeouts within ActiveRecord!
Installation
Add this line to your application's Gemfile:
gem 'activerecord-dynamic_timeout', require: 'active_record/dynamic_timeout/railtie'
Non-Rails Applications
If you are using this gem in a non-rails application, you will need run the initializer manually.
require "active_record/dynamic_timeout"
ActiveRecord::DynamicTimeout::Initializer.initialize!
Usage
To use this gem, you can set a timeout for a block of code that runs queries using ActiveRecord.
Within the block, if a query takes longer than the timeout value (in seconds), an ActiveRecord::QueryAborted
error (or a subclass of it) will be raised.
Example
class MyModel < ActiveRecord::Base
end
MyModel.all
MyModel.with_timeout(5.seconds) do
MyModel.all
end
Supported Adapters
- mysql2
- trilogy - ActiveRecord versions 7.1+
- postgresql
- sqlite3 (version >= 2.0) - Note - ActiveRecord < 7.1 does not support sqlite3 >= 2.0
See OtherAdapters on how to add support for other adapters.
Mysql2
Timeouts are set using the client read_timeout and write_timeout attributes on the client. At the moment this is a bit of a hack as the mysql2 gem doesn't provide
a clean way to set these attributes (via a public method).
A Pull Request has been open for over 6 years to add per-query read_timeouts: https://github.com/brianmario/mysql2/pull/955
No queries are executed to set the timeouts.
Warning on Raw Inserts and Updates
If you are using raw inserts or updates, ensure you wrap them in a transaction. If you do not, the timeout will still occur but the query on the server side will still continue.
If you are using normal ActiveRecord methods (e.g. MyModel.create
, MyModel.update
, etc.), you do not need to worry about this because these run the queries within a transaction already.
MyModel.count
MyModel.with_timeout(1.seconds) do
MyModel.connection.execute("INSERT INTO my_models SELECT SLEEP(2)")
end
MyModel.count
MyModel.with_timeout(1.seconds) do
MyModel.transaction do
MyModel.connection.execute("INSERT INTO my_models SELECT SLEEP(2)")
end
end
Trilogy
Timeouts are set via the client read_timeout and write_timeout attributes on the clients. No queries are executed to set the timeouts.
Warning on Raw Inserts and Updates (Trilogy)
See this section for more information.
Postgresql
Timeouts are set via setting the session variable via the following query SET SESSION statement_timeout TO <timeout>
.
See more information at https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-STATEMENT-TIMEOUT
Note - Because this executes a query to set the timeout, we will lazily set and reset the timeout on the connection. This is done to reduce
the number of queries run.
Sqlite3
Timeouts are set via the Sqlite3::Database#statement_timeout=
method. See more information at https://www.rubydoc.info/gems/sqlite3/SQLite3/Database:statement_timeout=
Under the hood, this sets a progress_handler that will check every 1000 virtual machine instructions if the timeout has been exceeded. If it has,
it will interrupt the query and raise out.
More information about Sqlite Progress Handlers: https://www.sqlite.org/draft/c3ref/progress_handler.html
More information about Sqlite interrupt: https://www.sqlite.org/c3ref/interrupt.html
Note - Because this executes a query to set the timeout, we will lazily set and reset the timeout on the connection. This is done to reduce
the number of queries run.
Other Adapters
If you would like to add support for a different adapter, add the following code to the adapter:
#supports_dynamic_timeouts?
- Must return true#set_connection_timeout(raw_connection, timeout)
- Set the timeout on the connection#reset_connection_timeout(raw_connection)
- Reset the timeout on the connection#timeout_set_client_side?
- Used to decide if the timeout should be set lazily (and reset) or not
class MyAdapter < ActiveRecord::ConnectionAdapters::AbstractAdapter
def supports_dynamic_timeouts?
true
end
def set_connection_timeout(raw_connection, timeout)
end
def reset_connection_timeout(raw_connection)
end
def timeout_set_client_side?
false
end
end
Contributing
Bug reports and pull requests are welcome on GitHub.