cutoff 1.0.0 → 1.2.0

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: ccf20a5afa9e52d5c638e5e72edfa1395375701b31525868ab354d036d6593a5
4
- data.tar.gz: 83f27ac46d53346b3fa5c9b07644dacb3ce058a78d7bd0e07a72e0eb4b291874
3
+ metadata.gz: 8e267a7090c7eee2e054ccc060c18a38a4f6d27277477d7fcfc3263b7f16091a
4
+ data.tar.gz: f7a34c730fe930080da8a539b5c864b4b4af467a042c7ddf075ba7df7242da79
5
5
  SHA512:
6
- metadata.gz: 5b13ffe6962c36d49aff8969bc1ca23e8e0173885505556d945dd19593b815261fc07202d3fa75d83433d3c6ad831645bcdaaf79e5a06785e1a4dc9aeceea955
7
- data.tar.gz: 165328cd0c983b3ec1b9007c0a4e17bd4e66b3dd4683f4a760e7374ec1c563caff1ffcd9dfe8653e8945da92764d110a7fb3ef7d66011d56d1f3f91cda7d4983
6
+ metadata.gz: 1853882757fc2da5bae7c836423893dcd2b673f09ff4ebb191dcababd8b44fbf114617664a7974bf67b2a615dbb2fb2f8c2da31876bf5a580d47f9b908d9af79
7
+ data.tar.gz: 75eec1648d14ac62203989bb51ab2b4e3e6710006c0c072d4e98933c367ebd96fde88b7a33705983f1ba09cc44b5e486dacde80e06b9e47291a804883c16872b
data/CHANGELOG.md CHANGED
@@ -7,6 +7,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.2.0] - 2026-08-07
11
+
12
+ ### Fixed
13
+
14
+ - The Mysql2 patch now finds the first `SELECT` when a statement begins with a
15
+ parenthesized query block, as in `(SELECT ...) UNION (SELECT ...)`.
16
+ Previously the leading `(` was treated as an unrecognized token and the
17
+ parser bailed out, so `MAX_EXECUTION_TIME` was silently never inserted and
18
+ the query ran with no server-side ceiling. MySQL applies a hint in the first
19
+ query block to the whole statement, so these queries are now capped
20
+ correctly.
21
+ - Fixed a check in the Mysql2 patch that used `String#casecmp` where a boolean
22
+ was expected. Because `casecmp` returns `0` for a match (which is truthy in
23
+ Ruby), the guard never fired and any leading token merely *starting with*
24
+ `select` (such as `selections`) had a hint comment spliced into it,
25
+ producing invalid SQL. It now uses `casecmp?`.
26
+
27
+ ### Upgrading
28
+
29
+ Queries beginning with a parenthesized query block were previously never given
30
+ a `MAX_EXECUTION_TIME` hint, so they ran unbounded on the server even with an
31
+ active cutoff. They are now capped, which means a query that used to slowly
32
+ succeed may instead raise `Mysql2::Error`. That is the intended behavior of the
33
+ patch, but it can surface as new errors on upgrade. If you have queries in this
34
+ shape, check that their callers handle the error, or give them a longer cutoff.
35
+
36
+ Statements beginning with `WITH` (common table expressions) are still not
37
+ given a hint.
38
+
39
+ ## [1.1.0] - 2026-05-12
40
+
41
+ ### Fixed
42
+
43
+ - Net::HTTP patch now also re-applies on `begin_transport`, so internal retries
44
+ (Net::HTTP retries idempotent requests once by default on transient errors
45
+ like Net::ReadTimeout) respect the cutoff instead of silently doubling the
46
+ effective deadline.
47
+
10
48
  ## [1.0.0] - 2026-05-12
11
49
 
12
50
  This release marks Cutoff's API as stable. There are no behavior changes
@@ -98,7 +136,9 @@ to `Timeout::Error`. `CutoffError` changes from a class to a module.
98
136
  - Cutoff class
99
137
  - Mysql2 patch
100
138
 
101
- [Unreleased]: https://github.com/justinhoward/cutoff/compare/v1.0.0...HEAD
139
+ [Unreleased]: https://github.com/justinhoward/cutoff/compare/v1.2.0...HEAD
140
+ [1.2.0]: https://github.com/justinhoward/cutoff/compare/v1.1.0...v1.2.0
141
+ [1.1.0]: https://github.com/justinhoward/cutoff/compare/v1.0.0...v1.1.0
102
142
  [1.0.0]: https://github.com/justinhoward/cutoff/compare/v0.5.2...v1.0.0
103
143
  [0.5.2]: https://github.com/justinhoward/cutoff/compare/v0.5.1...v0.5.2
104
144
  [0.5.1]: https://github.com/justinhoward/cutoff/compare/v0.5.0...v0.5.1
@@ -69,6 +69,8 @@ class Cutoff
69
69
  hint_comment
70
70
  elsif token.start_with?('/*')
71
71
  block_comment
72
+ elsif !@found_select && token.start_with?('(')
73
+ open_paren
72
74
  elsif token.match?(/^select/i)
73
75
  select
74
76
  else
@@ -133,6 +135,21 @@ class Cutoff
133
135
  @scanner.terminate
134
136
  end
135
137
 
138
+ def open_paren
139
+ # A statement can begin with a parenthesized query block, as in
140
+ # `(SELECT ...) UNION (SELECT ...)`. MySQL accepts an optimizer hint
141
+ # at the start of the first block and applies MAX_EXECUTION_TIME to
142
+ # the whole statement, so consume the parentheses and keep looking
143
+ # for the select.
144
+ #
145
+ # Go back to the beginning of the token and skip only the parens
146
+ # (and any whitespace after them) so the rest of the token is
147
+ # re-examined as its own token. This always advances the scanner by
148
+ # at least one character, so the token loop can't spin.
149
+ @scanner.unscan
150
+ @scanner.skip(/\(+\s*/)
151
+ end
152
+
136
153
  def select
137
154
  # If we encounter a select, we're ready to place our hint comment
138
155
  @scanner.unscan
@@ -140,7 +157,7 @@ class Cutoff
140
157
 
141
158
  # Make sure our word is actually select
142
159
  # We only checked that it starts with select before
143
- return other unless word.casecmp('select')
160
+ return other unless word.casecmp?('select')
144
161
 
145
162
  @found_select = true
146
163
  @hint_pos = @scanner.pos
@@ -25,22 +25,43 @@ class Cutoff
25
25
  end
26
26
 
27
27
  # Same as the original start, but adds a checkpoint for starting HTTP
28
- # requests and sets network timeouts to the remaining time
28
+ # requests and sets network timeouts to the remaining time.
29
+ #
30
+ # Also applies the same logic to begin_transport, which is called on
31
+ # every HTTP attempt including internal retries. Net::HTTP#transport_request
32
+ # silently retries idempotent requests (GET, HEAD, PUT, DELETE, OPTIONS,
33
+ # TRACE) up to max_retries (default 1) on transient errors like
34
+ # Net::ReadTimeout. Without re-applying the cutoff in begin_transport,
35
+ # the retry path goes through #connect (not #start), reuses the original
36
+ # read_timeout, and effectively doubles the deadline you set.
29
37
  #
30
38
  # @method start
39
+ # @method begin_transport
31
40
  # @see Net::HTTP#start
41
+ # @see Net::HTTP#begin_transport
32
42
  module_eval(<<~RUBY, __FILE__, __LINE__ + 1)
33
43
  def start
34
- if (cutoff = Cutoff.current) && cutoff.selected?(:net_http)
35
- remaining = cutoff.seconds_remaining
36
- #{gen_timeout_method('open_timeout')}
37
- #{gen_timeout_method('read_timeout')}
38
- #{gen_timeout_method('write_timeout') if use_write_timeout?}
39
- #{gen_timeout_method('continue_timeout')}
40
- Cutoff.checkpoint!(:net_http)
41
- end
44
+ _cutoff_apply_to_net_http
42
45
  super
43
46
  end
47
+
48
+ def begin_transport(req)
49
+ _cutoff_apply_to_net_http
50
+ super
51
+ end
52
+
53
+ private
54
+
55
+ def _cutoff_apply_to_net_http
56
+ return unless (cutoff = Cutoff.current) && cutoff.selected?(:net_http)
57
+
58
+ remaining = cutoff.seconds_remaining
59
+ #{gen_timeout_method('open_timeout')}
60
+ #{gen_timeout_method('read_timeout')}
61
+ #{gen_timeout_method('write_timeout') if use_write_timeout?}
62
+ #{gen_timeout_method('continue_timeout')}
63
+ Cutoff.checkpoint!(:net_http)
64
+ end
44
65
  RUBY
45
66
  end
46
67
  end
@@ -3,6 +3,6 @@
3
3
  class Cutoff
4
4
  # @return [Gem::Version] The current version of the cutoff gem
5
5
  def self.version
6
- Gem::Version.new('1.0.0')
6
+ Gem::Version.new('1.2.0')
7
7
  end
8
8
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cutoff
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Howard
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-05-12 00:00:00.000000000 Z
11
+ date: 2026-08-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -84,7 +84,7 @@ licenses:
84
84
  - MIT
85
85
  metadata:
86
86
  changelog_uri: https://github.com/justinhoward/cutoff/blob/master/CHANGELOG.md
87
- documentation_uri: https://www.rubydoc.info/gems/cutoff/1.0.0
87
+ documentation_uri: https://www.rubydoc.info/gems/cutoff/1.2.0
88
88
  rubygems_mfa_required: 'true'
89
89
  post_install_message:
90
90
  rdoc_options: []