activerecord-dynamic_timeout 0.0.1.rc2 → 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 55639dda91869cb0dc47c52543cd9becb00c99d0401fe3ba8de74895137798c6
4
- data.tar.gz: 7435229decada73df01abdaad9183da8d850512aeb30f2efe30148a0e6e9d981
3
+ metadata.gz: ddb706eb329656e5e5651e508e50720ad46051805d00ae10b49dbb7ff484d446
4
+ data.tar.gz: 249cfadad65543235ba52b251d56097c0b8180eaf1ae370400611da0989c5598
5
5
  SHA512:
6
- metadata.gz: 296df3e1d3484a39921f718ab1ed19ba23fdb009d7adba17cddb7e6320babb23b146cd08e3272bdde7260b150a12a942e75d56ff9405bcc806ba20aa404eb478
7
- data.tar.gz: 45ef043b9e41620cb3b8e2561b8f09b12d4d523001f0fa146eff7dbfd35c5db335d172954bbe2cc2d4b1acec743a70f3e0384a01af19549e0617c4910968d4e9
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.2"
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
@@ -0,0 +1,8 @@
1
+ # CHANGELOG for `activerecord-dynamic_timeout`
2
+
3
+ Inspired by [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
4
+
5
+ Note: this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## 0.1.0 (Unreleased)
8
+ - Initial release
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- activerecord-dynamic_timeout (0.0.1.rc2)
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.4-arm64-darwin)
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 Timeouts within ActiveRecord
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
- # encoding: utf-8
1
+ # frozen_string_literal: true
2
2
 
3
- $:.unshift File.expand_path("../lib", __FILE__)
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, timeout)
22
- return unless supports_dynamic_timeouts?
23
- return if active_record_dynamic_timeout == timeout
24
- if timeout.nil?
25
- reset_dynamic_timeout(raw_connection)
26
- else
27
- set_connection_timeout(raw_connection, timeout)
28
- self.active_record_dynamic_timeout = timeout
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
- return unless supports_dynamic_timeouts?
34
- return if active_record_dynamic_timeout.nil?
35
- reset_connection_timeout(raw_connection)
36
- self.active_record_dynamic_timeout = nil
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.current_timeout)
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.current_timeout)
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
- def with_timeout(timeout)
12
- (timeout.is_a?(Integer) || timeout.nil?) or raise ArgumentError, "timeout must be an Integer or NilClass, got: `#{timeout.inspect}`"
13
- timeout_stack << timeout
14
- yield
15
- ensure
16
- timeout_stack.pop
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
- def current_timeout
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, timeout)
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, timeout)
4
- if timeout.nil? || timeout == ":default" || timeout == :default
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
- set_connection_timeout(raw_connection, default_statement_timeout)
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, timeout)
6
- if timeout
7
- raw_connection.busy_timeout(timeout)
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.busy_timeout(0)
9
+ raw_connection.statement_timeout = 0
10
10
  end
11
11
  end
12
12
 
13
13
  def reset_connection_timeout(raw_connection)
14
- timeout = @config[:timeout]
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
- false
18
+ true
20
19
  end
21
20
 
22
21
  def supports_dynamic_timeouts?
23
- true
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, timeout)
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.prepend(extension) if extension
38
+ adapter_class.include(extension) if extension
40
39
  end
41
40
  end
42
41
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../dynamic_timeout"
3
4
  require_relative "initializer"
4
5
 
5
6
  module ActiveRecord::DynamicTimeout
@@ -1,5 +1,5 @@
1
1
  module ActiveRecord
2
2
  module DynamicTimeout
3
- VERSION = '0.0.1.rc2'
3
+ VERSION = '0.0.1'
4
4
  end
5
5
  end
@@ -3,6 +3,9 @@
3
3
  require "active_record"
4
4
  require "active_support"
5
5
 
6
+ require_relative "dynamic_timeout/version"
7
+ require_relative "dynamic_timeout/initializer"
8
+
6
9
  module ActiveRecord
7
10
  module DynamicTimeout
8
11
  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.rc2
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-22 00:00:00.000000000 Z
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: 1.3.1
72
+ version: '0'
72
73
  requirements: []
73
74
  rubygems_version: 3.3.27
74
75
  signing_key: