activerecord-dynamic_timeout 0.0.1.rc2 → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Appraisals +1 -7
- data/CHANGELOG.md +8 -0
- data/Gemfile.lock +2 -2
- data/README.md +142 -1
- data/activerecord-dynamic_timeout.gemspec +3 -2
- data/lib/active_record/dynamic_timeout/extensions/abstract_adapter_extension.rb +14 -15
- data/lib/active_record/dynamic_timeout/extensions/base_extension.rb +11 -7
- data/lib/active_record/dynamic_timeout/extensions/mysql2_adapter_extension.rb +2 -1
- data/lib/active_record/dynamic_timeout/extensions/postgres_adapter_extension.rb +15 -3
- data/lib/active_record/dynamic_timeout/extensions/sqlite_adapter_extension.rb +7 -8
- data/lib/active_record/dynamic_timeout/extensions/trilogy_adapter_extension.rb +2 -1
- data/lib/active_record/dynamic_timeout/initializer.rb +1 -2
- data/lib/active_record/dynamic_timeout/railtie.rb +1 -0
- data/lib/active_record/dynamic_timeout/version.rb +1 -1
- data/lib/active_record/dynamic_timeout.rb +3 -0
- metadata +5 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ddb706eb329656e5e5651e508e50720ad46051805d00ae10b49dbb7ff484d446
|
4
|
+
data.tar.gz: 249cfadad65543235ba52b251d56097c0b8180eaf1ae370400611da0989c5598
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ae50c77306307acf78925ad6c124630b49629614f47b370397e156ea100ef8090c6dd986197e6d6a1be84d4bed38a3a7f33dedb67c1944b3f7a6a82bb58510c7
|
7
|
+
data.tar.gz: 2af7ea2c782e232433571e79ed8e2a4f60a2f85acd646de338e89ec5274e71744cef9acd49a8ae3263748edbcee35de2fd46f77cee4aebf0d4786f0b886ce6f0
|
data/Appraisals
CHANGED
@@ -3,13 +3,7 @@
|
|
3
3
|
require "appraisal/matrix"
|
4
4
|
|
5
5
|
appraisal_matrix(activerecord: "6.1") do |activerecord:|
|
6
|
-
if activerecord < "7.
|
6
|
+
if activerecord < "7.1"
|
7
7
|
gem "sqlite3", "~> 1.4"
|
8
8
|
end
|
9
9
|
end
|
10
|
-
# appraisal_matrix(activerecord: "6.1", mysql2: { versions: ["~> 0.5"], step: :major })
|
11
|
-
# appraisal_matrix(activerecord: "6.1", pg: { versions: ["~> 1.5"], step: :major })
|
12
|
-
#
|
13
|
-
# appraisal_matrix(activerecord: [">= 6.1", "< 7.2"], sqlite3: { versions: ["~> 1.4"], step: :major })
|
14
|
-
# appraisal_matrix(activerecord: "7.2", sqlite3: { versions: [">= 1.4"], step: :major })
|
15
|
-
# appraisal_matrix(activerecord: "7.1", trilogy: { versions: [">= 2.8"], step: :major })
|
data/CHANGELOG.md
ADDED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
activerecord-dynamic_timeout (0.0.1
|
4
|
+
activerecord-dynamic_timeout (0.0.1)
|
5
5
|
activerecord (>= 6.1)
|
6
6
|
|
7
7
|
GEM
|
@@ -70,7 +70,7 @@ GEM
|
|
70
70
|
rspec-support (~> 3.13.0)
|
71
71
|
rspec-support (3.13.1)
|
72
72
|
securerandom (0.3.1)
|
73
|
-
sqlite3 (2.0.
|
73
|
+
sqlite3 (2.0.0-arm64-darwin)
|
74
74
|
stringio (3.1.1)
|
75
75
|
thor (1.3.2)
|
76
76
|
timeout (0.4.1)
|
data/README.md
CHANGED
@@ -1,2 +1,143 @@
|
|
1
1
|
# activerecord-dynamic_timeout
|
2
|
-
Dynamic
|
2
|
+
Dynamic query timeouts within ActiveRecord!
|
3
|
+
|
4
|
+
## Installation
|
5
|
+
|
6
|
+
Add this line to your application's Gemfile:
|
7
|
+
```ruby
|
8
|
+
gem 'activerecord-dynamic_timeout', require: 'active_record/dynamic_timeout/railtie'
|
9
|
+
```
|
10
|
+
|
11
|
+
### Non-Rails Applications
|
12
|
+
If you are using this gem in a non-rails application, you will need run the initializer manually.
|
13
|
+
```ruby
|
14
|
+
require "active_record/dynamic_timeout"
|
15
|
+
ActiveRecord::DynamicTimeout::Initializer.initialize!
|
16
|
+
```
|
17
|
+
|
18
|
+
## Usage
|
19
|
+
To use this gem, you can set a timeout for a block of code that runs queries using ActiveRecord.
|
20
|
+
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.
|
21
|
+
|
22
|
+
#### Example
|
23
|
+
```ruby
|
24
|
+
class MyModel < ActiveRecord::Base
|
25
|
+
# Model code...
|
26
|
+
end
|
27
|
+
|
28
|
+
MyModel.all # A long query that takes over 5 seconds
|
29
|
+
# => ActiveRecord::Relation<MyModel>...
|
30
|
+
|
31
|
+
# Set a timeout for all queries run within the block
|
32
|
+
MyModel.with_timeout(5.seconds) do
|
33
|
+
MyModel.all # A long running query that takes over 5 seconds
|
34
|
+
end
|
35
|
+
# => Raises ActiveRecord::QueryAborted error (or a subclass of it) after 5 seconds.
|
36
|
+
```
|
37
|
+
|
38
|
+
## Supported Adapters
|
39
|
+
* mysql2
|
40
|
+
* trilogy - ActiveRecord versions 7.1+
|
41
|
+
* postgresql
|
42
|
+
* sqlite3 (version >= 2.0) - Note - ActiveRecord < 7.1 does not support sqlite3 >= 2.0
|
43
|
+
|
44
|
+
See [OtherAdapters](#other-adapters) on how to add support for other adapters.
|
45
|
+
|
46
|
+
### Mysql2
|
47
|
+
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
|
48
|
+
a clean way to set these attributes (via a public method).
|
49
|
+
|
50
|
+
A Pull Request has been open for over 6 years to add per-query read_timeouts: https://github.com/brianmario/mysql2/pull/955
|
51
|
+
|
52
|
+
No queries are executed to set the timeouts.
|
53
|
+
|
54
|
+
#### Warning on Raw Inserts and Updates
|
55
|
+
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.
|
56
|
+
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.
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
### Bad!!!
|
60
|
+
MyModel.count
|
61
|
+
# => 0
|
62
|
+
MyModel.with_timeout(1.seconds) do
|
63
|
+
MyModel.connection.execute("INSERT INTO my_models SELECT SLEEP(2)") # This will take longer than 1 second and cause a timeout.
|
64
|
+
end
|
65
|
+
# Wait ~1-2 seconds...
|
66
|
+
MyModel.count
|
67
|
+
# => 1 # The query still completed on the server side even though the client has timed out.
|
68
|
+
|
69
|
+
### Good
|
70
|
+
# Wrap the raw query in a transaction
|
71
|
+
# This will cause the query to be rolled back if the timeout occurs.
|
72
|
+
MyModel.with_timeout(1.seconds) do
|
73
|
+
MyModel.transaction do
|
74
|
+
MyModel.connection.execute("INSERT INTO my_models SELECT SLEEP(2)") # This will take longer than 1 second and cause a timeout.
|
75
|
+
end
|
76
|
+
end
|
77
|
+
```
|
78
|
+
|
79
|
+
### Trilogy
|
80
|
+
Timeouts are set via the client read_timeout and write_timeout attributes on the clients. No queries are executed to set the timeouts.
|
81
|
+
|
82
|
+
#### Warning on Raw Inserts and Updates (Trilogy)
|
83
|
+
See [this section](#warning-on-raw-inserts-and-updates) for more information.
|
84
|
+
|
85
|
+
### Postgresql
|
86
|
+
Timeouts are set via setting the session variable via the following query `SET SESSION statement_timeout TO <timeout>`.
|
87
|
+
|
88
|
+
See more information at https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-STATEMENT-TIMEOUT
|
89
|
+
|
90
|
+
**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
|
91
|
+
the number of queries run.
|
92
|
+
|
93
|
+
### Sqlite3
|
94
|
+
Timeouts are set via the `Sqlite3::Database#statement_timeout=` method. See more information at https://www.rubydoc.info/gems/sqlite3/SQLite3/Database:statement_timeout=
|
95
|
+
|
96
|
+
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,
|
97
|
+
it will interrupt the query and raise out.
|
98
|
+
|
99
|
+
More information about Sqlite Progress Handlers: https://www.sqlite.org/draft/c3ref/progress_handler.html
|
100
|
+
|
101
|
+
More information about Sqlite interrupt: https://www.sqlite.org/c3ref/interrupt.html
|
102
|
+
|
103
|
+
**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
|
104
|
+
the number of queries run.
|
105
|
+
|
106
|
+
### Other Adapters
|
107
|
+
If you would like to add support for a different adapter, add the following code to the adapter:
|
108
|
+
1. `#supports_dynamic_timeouts?` - Must return true
|
109
|
+
2. `#set_connection_timeout(raw_connection, timeout)` - Set the timeout on the connection
|
110
|
+
3. `#reset_connection_timeout(raw_connection)` - Reset the timeout on the connection
|
111
|
+
4. `#timeout_set_client_side?` - Used to decide if the timeout should be set lazily (and reset) or not
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
class MyAdapter < ActiveRecord::ConnectionAdapters::AbstractAdapter
|
115
|
+
# @return [Boolean]
|
116
|
+
def supports_dynamic_timeouts?
|
117
|
+
true # Must return true.
|
118
|
+
end
|
119
|
+
|
120
|
+
# @param raw_connection [Object] The raw connection object of the adapter
|
121
|
+
# @param timeout [Integer] The timeout passed in by the user
|
122
|
+
def set_connection_timeout(raw_connection, timeout)
|
123
|
+
# Set the timeout on the connection
|
124
|
+
end
|
125
|
+
|
126
|
+
# @param raw_connection [Object] The raw connection object of the adapter
|
127
|
+
def reset_connection_timeout(raw_connection)
|
128
|
+
# Reset the timeout on the connection, to the default value or the value set in the database configuration file.
|
129
|
+
end
|
130
|
+
|
131
|
+
# @return [Boolean]
|
132
|
+
def timeout_set_client_side?
|
133
|
+
false
|
134
|
+
# Return true if the timeout does not require a query to be executed in order to set the timeout
|
135
|
+
# Return false if the timeout requires a query to be executed in order to set the timeout. When false, the timeout will be set lazily, only when necessary.
|
136
|
+
end
|
137
|
+
|
138
|
+
# Adapter code...
|
139
|
+
end
|
140
|
+
```
|
141
|
+
|
142
|
+
## Contributing
|
143
|
+
Bug reports and pull requests are welcome on GitHub.
|
@@ -1,6 +1,7 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
lib = File.expand_path("lib", __dir__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
5
|
require "active_record/dynamic_timeout/version"
|
5
6
|
|
6
7
|
Gem::Specification.new do |s|
|
@@ -2,7 +2,6 @@
|
|
2
2
|
|
3
3
|
require "active_support"
|
4
4
|
require "active_support/concern"
|
5
|
-
require_relative "../../dynamic_timeout"
|
6
5
|
|
7
6
|
module ActiveRecord::DynamicTimeout
|
8
7
|
module AbstractAdapterExtension
|
@@ -18,29 +17,29 @@ module ActiveRecord::DynamicTimeout
|
|
18
17
|
false
|
19
18
|
end
|
20
19
|
|
21
|
-
def set_dynamic_timeout(raw_connection,
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
20
|
+
def set_dynamic_timeout(raw_connection, timeout_seconds)
|
21
|
+
if supports_dynamic_timeouts? && timeout_seconds != active_record_dynamic_timeout
|
22
|
+
if timeout_seconds.nil?
|
23
|
+
reset_dynamic_timeout(raw_connection)
|
24
|
+
else
|
25
|
+
set_connection_timeout(raw_connection, timeout_seconds)
|
26
|
+
self.active_record_dynamic_timeout = timeout_seconds
|
27
|
+
end
|
29
28
|
end
|
30
29
|
end
|
31
30
|
|
32
31
|
def reset_dynamic_timeout(raw_connection)
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
32
|
+
if supports_dynamic_timeouts? && active_record_dynamic_timeout
|
33
|
+
reset_connection_timeout(raw_connection)
|
34
|
+
self.active_record_dynamic_timeout = nil
|
35
|
+
end
|
37
36
|
end
|
38
37
|
end
|
39
38
|
|
40
39
|
module TimeoutAdapterExtension
|
41
40
|
def with_raw_connection(*args, **kwargs, &block)
|
42
41
|
super do |raw_connection|
|
43
|
-
set_dynamic_timeout(raw_connection, ActiveRecord::Base.
|
42
|
+
set_dynamic_timeout(raw_connection, ActiveRecord::Base.current_timeout_seconds)
|
44
43
|
yield raw_connection
|
45
44
|
ensure
|
46
45
|
if timeout_set_client_side?
|
@@ -69,7 +68,7 @@ module ActiveRecord::DynamicTimeout
|
|
69
68
|
module TimeoutAdapterExtension_Rails_7_0
|
70
69
|
def log(*args, **kwargs, &block)
|
71
70
|
super do
|
72
|
-
set_dynamic_timeout(@connection, ActiveRecord::Base.
|
71
|
+
set_dynamic_timeout(@connection, ActiveRecord::Base.current_timeout_seconds)
|
73
72
|
yield
|
74
73
|
ensure
|
75
74
|
if timeout_set_client_side?
|
@@ -8,15 +8,19 @@ module ActiveRecord::DynamicTimeout
|
|
8
8
|
extend ActiveSupport::Concern
|
9
9
|
|
10
10
|
module ClassMethods
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
11
|
+
# @param [Numeric, NilClass] timeout_seconds The timeout in seconds, or nil to disable the timeout.
|
12
|
+
def with_timeout(timeout_seconds)
|
13
|
+
(timeout_seconds.is_a?(Numeric) || timeout_seconds.nil?) or raise ArgumentError, "timeout_seconds must be Numeric or nil, got: `#{timeout_seconds.inspect}`"
|
14
|
+
begin
|
15
|
+
timeout_stack << timeout_seconds
|
16
|
+
yield
|
17
|
+
ensure
|
18
|
+
timeout_stack.pop
|
19
|
+
end
|
17
20
|
end
|
18
21
|
|
19
|
-
|
22
|
+
# @return [Numeric, NilClass] The current timeout in seconds, or nil if no timeout is set.
|
23
|
+
def current_timeout_seconds
|
20
24
|
timeout_stack.last
|
21
25
|
end
|
22
26
|
|
@@ -1,6 +1,7 @@
|
|
1
1
|
module ActiveRecord::DynamicTimeout
|
2
2
|
module Mysql2AdapterExtension
|
3
|
-
def set_connection_timeout(raw_connection,
|
3
|
+
def set_connection_timeout(raw_connection, timeout_seconds)
|
4
|
+
timeout = timeout_seconds.ceil.to_i # Round floats up to the nearest integer
|
4
5
|
set_timeouts_on_connection(raw_connection, read_timeout: timeout, write_timeout: timeout)
|
5
6
|
end
|
6
7
|
|
@@ -1,15 +1,21 @@
|
|
1
1
|
module ActiveRecord::DynamicTimeout
|
2
2
|
module PostgresAdapterExtension
|
3
|
-
def set_connection_timeout(raw_connection,
|
4
|
-
if
|
3
|
+
def set_connection_timeout(raw_connection, timeout_seconds)
|
4
|
+
if set_to_default_timeout?(timeout_seconds)
|
5
5
|
raw_connection.query("SET SESSION statement_timeout TO DEFAULT")
|
6
6
|
else
|
7
|
+
timeout = (timeout_seconds * 1000).to_i
|
7
8
|
raw_connection.query("SET SESSION statement_timeout TO #{quote(timeout)}")
|
8
9
|
end
|
9
10
|
end
|
10
11
|
|
11
12
|
def reset_connection_timeout(raw_connection)
|
12
|
-
|
13
|
+
timeout = default_statement_timeout
|
14
|
+
if set_to_default_timeout?(timeout)
|
15
|
+
raw_connection.query("SET SESSION statement_timeout TO DEFAULT")
|
16
|
+
else
|
17
|
+
raw_connection.query("SET SESSION statement_timeout TO #{quote(timeout)}")
|
18
|
+
end
|
13
19
|
end
|
14
20
|
|
15
21
|
def timeout_set_client_side?
|
@@ -22,6 +28,12 @@ module ActiveRecord::DynamicTimeout
|
|
22
28
|
|
23
29
|
private
|
24
30
|
|
31
|
+
# This method is copying how Rails configures session variables from the database config file.
|
32
|
+
# https://github.com/rails/rails/blob/main/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb#L970-L984
|
33
|
+
def set_to_default_timeout?(timeout)
|
34
|
+
timeout.nil? || timeout == ":default" || timeout == :default
|
35
|
+
end
|
36
|
+
|
25
37
|
def default_statement_timeout
|
26
38
|
unless defined?(@default_statement_timeout)
|
27
39
|
@default_statement_timeout = @config.fetch(:variables, {}).stringify_keys["statement_timeout"]
|
@@ -2,25 +2,24 @@
|
|
2
2
|
|
3
3
|
module ActiveRecord::DynamicTimeout
|
4
4
|
module SqliteAdapterExtension
|
5
|
-
def set_connection_timeout(raw_connection,
|
6
|
-
if
|
7
|
-
raw_connection.
|
5
|
+
def set_connection_timeout(raw_connection, timeout_seconds)
|
6
|
+
if timeout_seconds
|
7
|
+
raw_connection.statement_timeout = (timeout_seconds * 1000).to_i
|
8
8
|
else
|
9
|
-
raw_connection.
|
9
|
+
raw_connection.statement_timeout = 0
|
10
10
|
end
|
11
11
|
end
|
12
12
|
|
13
13
|
def reset_connection_timeout(raw_connection)
|
14
|
-
|
15
|
-
set_connection_timeout(raw_connection, self.class.type_cast_config_to_integer(timeout))
|
14
|
+
raw_connection.statement_timeout = 0
|
16
15
|
end
|
17
16
|
|
18
17
|
def timeout_set_client_side?
|
19
|
-
|
18
|
+
true
|
20
19
|
end
|
21
20
|
|
22
21
|
def supports_dynamic_timeouts?
|
23
|
-
|
22
|
+
SQLite3::VERSION >= "2"
|
24
23
|
end
|
25
24
|
end
|
26
25
|
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
module ActiveRecord::DynamicTimeout
|
2
2
|
module TrilogyAdapterExtension
|
3
|
-
def set_connection_timeout(raw_connection,
|
3
|
+
def set_connection_timeout(raw_connection, timeout_seconds)
|
4
|
+
timeout = timeout_seconds.ceil.to_i # Round floats up to the nearest integer
|
4
5
|
set_timeouts_on_connection(raw_connection, read_timeout: timeout, write_timeout: timeout)
|
5
6
|
end
|
6
7
|
|
@@ -2,7 +2,6 @@
|
|
2
2
|
|
3
3
|
require "active_record"
|
4
4
|
require "active_support"
|
5
|
-
require_relative "../dynamic_timeout"
|
6
5
|
require_relative "extensions/base_extension"
|
7
6
|
require_relative "extensions/abstract_adapter_extension"
|
8
7
|
require_relative "extensions/mysql2_adapter_extension"
|
@@ -36,7 +35,7 @@ module ActiveRecord::DynamicTimeout
|
|
36
35
|
when "ActiveRecord::ConnectionAdapters::PostgreSQLAdapter"
|
37
36
|
ActiveRecord::DynamicTimeout::PostgresAdapterExtension
|
38
37
|
end
|
39
|
-
adapter_class.
|
38
|
+
adapter_class.include(extension) if extension
|
40
39
|
end
|
41
40
|
end
|
42
41
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activerecord-dynamic_timeout
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.1
|
4
|
+
version: 0.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tristan Starck
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-09
|
11
|
+
date: 2024-10-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -33,6 +33,7 @@ extensions: []
|
|
33
33
|
extra_rdoc_files: []
|
34
34
|
files:
|
35
35
|
- Appraisals
|
36
|
+
- CHANGELOG.md
|
36
37
|
- Gemfile
|
37
38
|
- Gemfile.lock
|
38
39
|
- LICENSE.md
|
@@ -66,9 +67,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
66
67
|
version: '0'
|
67
68
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
68
69
|
requirements:
|
69
|
-
- - "
|
70
|
+
- - ">="
|
70
71
|
- !ruby/object:Gem::Version
|
71
|
-
version:
|
72
|
+
version: '0'
|
72
73
|
requirements: []
|
73
74
|
rubygems_version: 3.3.27
|
74
75
|
signing_key:
|