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 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: