cutoff 0.1.0 → 0.4.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: e27bb02cc23dc725a4bcd3b9585159723e3fa4d3c88f98ec207ff2b91cf438d4
4
- data.tar.gz: 8ab5a3a462bbbb2a32837e69de70d55ac59b3e347ead1db889d374f2be5b9224
3
+ metadata.gz: 5ac27ce016591276e55afa82f64c08de370a969cb6552cc0d5ae69093d3a9e0c
4
+ data.tar.gz: 31e989979ae04de967a22e8d2457d6da654862603f380dd7521e415c9220148b
5
5
  SHA512:
6
- metadata.gz: c957634f0bc03f9fa8fb058080338978322adb54fce2c2c952c7b684ea3a979165f076abdc29878e416a084acd7b904ad6e8d637f0c1b90091ffa179fdb6076d
7
- data.tar.gz: b9ac92d37812b341fba24c6a3feb5c0f1f4c9265f1995aa5266b85e3d46e27c48e80fd4801ecc4f46d9a22b5195e0b1dfc35bc9c74449e2ac35c11ace28a5f15
6
+ metadata.gz: 4241c63fc4c647c44860aa0781e2966a5eadc1d7edea259851018308d1877d664c46a8d90bd5553f725d2e1fa5f767995b0f27327482bf2ce5128701908aa684
7
+ data.tar.gz: 64d599559055f6d8b3c384b88b5268b02317784bd44fd6be2384fd3c56b1f9da5a9cfb435c4af05c083ba20af6d8865f0c73a8fc46afff261ad70d43fc9c35e4
data/CHANGELOG.md CHANGED
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.1] - 2021-10-02
11
+
12
+ - Fix Net::HTTP patch to override timeouts given to start
13
+
14
+ ## [0.4.0] - 2021-10-01
15
+
16
+ - Add benchmarks and slight performance improvements
17
+ - Add Rails controller integration
18
+
19
+ ## [0.3.0] - 2021-08-20
20
+
21
+ - Allow timers to be disabled globally with `Cutoff.disable!`
22
+
23
+ ## [0.2.0] - 2021-07-22
24
+
25
+ ### Added
26
+
27
+ - Net::HTTP patch
28
+
10
29
  ## [0.1.0] - 2021-07-19
11
30
 
12
31
  ### Added
@@ -14,4 +33,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
14
33
  - Cutoff class
15
34
  - Mysql2 patch
16
35
 
36
+ [Unreleased]: https://github.com/justinhoward/cutoff/compare/v0.4.1...HEAD
37
+ [0.4.0]: https://github.com/justinhoward/cutoff/compare/v0.4.0...v0.4.1
38
+ [0.4.0]: https://github.com/justinhoward/cutoff/compare/v0.3.0...v0.4.0
39
+ [0.3.0]: https://github.com/justinhoward/cutoff/compare/v0.2.0...v0.3.0
40
+ [0.2.0]: https://github.com/justinhoward/cutoff/compare/v0.1.0...v0.2.0
17
41
  [0.1.0]: https://github.com/justinhoward/cutoff/releases/tag/v0.1.0
data/README.md CHANGED
@@ -20,8 +20,8 @@ Cutoff.wrap(5) do
20
20
  end
21
21
  ```
22
22
 
23
- It has a built-in patch for Mysql2 to auto-insert checkpoints and timeout query
24
- hints.
23
+ It has built-in patches for Mysql2 and Net::HTTP to auto-insert checkpoints and
24
+ timeouts.
25
25
 
26
26
  ```ruby
27
27
  require 'cutoff/patch/mysql2'
@@ -50,7 +50,7 @@ single query will take longer than 3 seconds. However, imagine a bad controller
50
50
  action or background job executes 100 slow queries. In that case, the queries
51
51
  add up to 300 seconds, much too long.
52
52
 
53
- Deadlines keep track of the total elapsed time in a request of job and interrupt
53
+ Deadlines keep track of the total elapsed time in a request or job and interrupt
54
54
  it if it takes too long.
55
55
 
56
56
  Installation
@@ -138,8 +138,11 @@ Patches
138
138
  -------------
139
139
 
140
140
  Cutoff is in early stages, but it aims to provide patches for common networked
141
- dependencies. The first of these is the `mysql2` patch. It is not loaded by
142
- default, so you need to require it manually.
141
+ dependencies. Patches automatically insert useful checkpoints and timeouts. The
142
+ patches so far are for `mysql2` and `Net::HTTP`. They are not loaded by default,
143
+ so you need to require them manually.
144
+
145
+ For example, to load the Mysql2 patch:
143
146
 
144
147
  ```ruby
145
148
  # In your Gemfile
@@ -152,10 +155,14 @@ require 'cutoff'
152
155
  require 'cutoff/patch/mysql2'
153
156
  ```
154
157
 
155
- Once it is enabled, any `Mysql2::Client` object will respect the current cutoff
156
- if one is set.
158
+ ### Mysql2
159
+
160
+ Once it is enabled, any `Mysql2::Client` object will respect the current
161
+ class-level cutoff if one is set.
157
162
 
158
163
  ```ruby
164
+ require 'cutoff/patch/mysql2'
165
+
159
166
  client = Mysql2::Client.new
160
167
  Cutoff.wrap(3) do
161
168
  sleep(4)
@@ -183,28 +190,82 @@ Cutoff.wrap(3) do
183
190
  end
184
191
  ```
185
192
 
193
+ ### Net::HTTP
194
+
195
+ Once it is enabled, any `Net::HTTP` requests will respect the current
196
+ class-level cutoff if one is set.
197
+
198
+ ```ruby
199
+ require 'cutoff/patch/net_http'
200
+
201
+ Cutoff.wrap(3) do
202
+ sleep(5)
203
+
204
+ # The cutoff is expired, so this hits a checkpoint and will not be executed
205
+ Net::HTTP.get(URI.parse('http://example.com'))
206
+ end
207
+
208
+ Cutoff.wrap(3) do
209
+ sleep(1.5)
210
+
211
+ # The cutoff has 1.5 seconds left, so this request will be executed
212
+ # open_timeout, read_timeout, and write_timeout (Ruby >= 2.6) will each
213
+ # be set to 1.5
214
+ # This means the overall time can be > 1.5 since the combined phases can take
215
+ # up to 4.5 seconds
216
+ Net::HTTP.get(URI.parse('http://example.com'))
217
+ end
218
+ ```
219
+
186
220
  Timing a Rails Controller
187
221
  ---------------------------
188
222
 
189
- One use of a cutoff is to add a deadline to a Rails controller action.
223
+ One use of a cutoff is to add a deadline to a Rails controller action. This is
224
+ typically preferable to approaches like `Rack::Timeout` that use the dangerous
225
+ `Timeout` class.
226
+
227
+ Cutoff includes a built-in integration for this purpose. If Rails is installed,
228
+ the `#cutoff` class method is available in your controllers.
190
229
 
191
230
  ```ruby
192
- around_action { |_controller, action| Cutoff.wrap(2.5) { action.call } }
231
+ class ApplicationController < ActionController::Base
232
+ # You may want to set a long global cutoff, but it's not required
233
+ cutoff 30
234
+ end
235
+
236
+ class UsersController < ApplicationController
237
+ cutoff 5.0
238
+
239
+ def index
240
+ # Now in your action, you can call `checkpoint!`, or if you're using the
241
+ # patches, checkpoints will be added automatically
242
+ Cutoff.checkpoint!
243
+ end
244
+ end
193
245
  ```
194
246
 
195
- Now in your action, you can call `checkpoint!`, or if you're using the Mysql2
196
- patch, checkpoints will be added automatically.
247
+ Just like with controller filters, you can use filters with the cutoff method.
197
248
 
198
249
  ```ruby
199
- def index
200
- # Do thing one
201
- Cutoff.checkpoint!
250
+ class UsersController < ApplicationController
251
+ # For example, use an :only filter
252
+ cutoff 5.0, only: :index
202
253
 
203
- # Do something else
254
+ # Multiple calls work just fine. Last match wins
255
+ cutoff 2.5, only: :show
256
+
257
+ def index
258
+ # ...
259
+ end
260
+
261
+ def show
262
+ # ...
263
+ end
204
264
  end
205
265
  ```
206
266
 
207
- Consider adding a global error handler for the `Cutoff::CutoffExceededError`
267
+ Consider adding a global error handler for the `Cutoff::CutoffExceededError` in
268
+ case you want to display a nice error page for timeouts.
208
269
 
209
270
  ```ruby
210
271
  class ApplicationController < ActionController::Base
@@ -216,6 +277,22 @@ class ApplicationController < ActionController::Base
216
277
  end
217
278
  ```
218
279
 
280
+ Disabling Cutoff for Testing and Development
281
+ ------------
282
+
283
+ When testing or debugging an application that uses Cutoff, you may want to
284
+ disable Cutoff entirely. These methods are not thread-safe and not intended for
285
+ production.
286
+
287
+ ```ruby
288
+ # This disables all cutoff timers, for both global and local instances
289
+ Cutoff.disable!
290
+ Cutoff.disabled? # => true
291
+
292
+ # Re-enable cutoff
293
+ Cutoff.enable!
294
+ ```
295
+
219
296
  Multi-threading
220
297
  -----------------
221
298
 
@@ -360,10 +437,9 @@ never interrupt a running program unless:
360
437
  - `checkpoint!` is called
361
438
  - a network timeout is exceeded
362
439
 
363
- Patches such as the current Mysql2 patch are designed to ease the burden on
364
- developers to manually call `checkpoint!` or configure network timeouts. The
365
- ruby `Timeout` class is not used. See Julia Evans' post on [Why Ruby's Timeout
366
- is dangerous][julia_evans].
440
+ Patches are designed to ease the burden on developers to manually call
441
+ `checkpoint!` or configure network timeouts. The ruby `Timeout` class is not
442
+ used. See Julia Evans' post on [Why Ruby's Timeout is dangerous][julia_evans].
367
443
 
368
444
  Patches are only applied by explicit opt-in, and Cutoff can always be used as a
369
445
  standalone library.
@@ -45,8 +45,8 @@ class Cutoff
45
45
 
46
46
  # Loop through tokens like "WORD " or "/* "
47
47
  while @scanner.scan(/(\S+)\s+/)
48
- # Get the word part. None of our tokens care about case
49
- handle_token(@scanner[1].downcase)
48
+ # Get the word part
49
+ handle_token(@scanner[1])
50
50
  end
51
51
 
52
52
  return @scanner.string unless @found_select
@@ -68,7 +68,7 @@ class Cutoff
68
68
  hint_comment
69
69
  elsif token.start_with?('/*')
70
70
  block_comment
71
- elsif token.start_with?('select')
71
+ elsif token.match?(/^select/i)
72
72
  select
73
73
  else
74
74
  other
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal:true
2
+
3
+ require 'net/http'
4
+
5
+ class Cutoff
6
+ module Patch
7
+ module NetHttp
8
+ def self.gen_timeout_method(name)
9
+ <<~RUBY
10
+ if #{name}.nil? || #{name} > remaining
11
+ self.#{name} = cutoff.seconds_remaining
12
+ end
13
+ RUBY
14
+ end
15
+
16
+ def self.use_write_timeout?
17
+ Gem::Version.new(RUBY_VERSION) > Gem::Version.new('2.6')
18
+ end
19
+
20
+ # Same as the original start, but adds a checkpoint for starting HTTP
21
+ # requests and sets network timeouts to the remaining time
22
+ #
23
+ # @see Net::HTTP#start
24
+ module_eval(<<~RUBY, __FILE__, __LINE__ + 1)
25
+ def start
26
+ if (cutoff = Cutoff.current)
27
+ remaining = cutoff.seconds_remaining
28
+ #{gen_timeout_method('open_timeout')}
29
+ #{gen_timeout_method('read_timeout')}
30
+ #{gen_timeout_method('write_timeout') if use_write_timeout?}
31
+ #{gen_timeout_method('continue_timeout')}
32
+ Cutoff.checkpoint!
33
+ end
34
+ super
35
+ end
36
+ RUBY
37
+ end
38
+ end
39
+ end
40
+
41
+ Net::HTTP.prepend(Cutoff::Patch::NetHttp)
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'action_controller'
4
+
5
+ class Cutoff
6
+ module Rails
7
+ # Rails controller integration
8
+ module Controller
9
+ # Set a cutoff for the controller
10
+ #
11
+ # Can be called multiple times with different options to configure
12
+ # cutoffs for various conditions. If multiple conditions match a given
13
+ # controller, the last applied cutoff "wins".
14
+ #
15
+ # @example
16
+ # class ApplicationController
17
+ # # Apply a global maximum
18
+ # cutoff 30
19
+ # end
20
+ #
21
+ # class UsersController < ApplicationController
22
+ # # Override the base time limit
23
+ # cutoff 5.0
24
+ # cutoff 3.0, only: :show
25
+ # cutoff 7, if: :signed_in
26
+ # end
27
+ #
28
+ # @param seconds [Float, Integer] The allowed seconds for a controller
29
+ # action
30
+ # @param options [Hash] Options to pass to `around_action`. For example,
31
+ # pass `:only`, `:except`, `:if`, to limit the scope of the cutoff.
32
+ def cutoff(seconds, options = {})
33
+ prepend_around_action(options) do |_controller, action|
34
+ next action.call if @cutoff_wrapped
35
+
36
+ begin
37
+ @cutoff_wrapped = true
38
+ Cutoff.wrap(seconds, &action)
39
+ ensure
40
+ @cutoff_wrapped = false
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+ ActionController::Base.extend(Cutoff::Rails::Controller)
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Namespace for Rails integration
4
+ module Rails
5
+ end
6
+
7
+ require 'cutoff/rails/controller' if Gem.loaded_specs['actionpack']
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal:true
2
+
3
+ class Cutoff
4
+ module Timer
5
+ if defined?(Process::CLOCK_MONOTONIC_RAW)
6
+ # The current time
7
+ #
8
+ # If it is available, this will use a monotonic clock. This is a clock
9
+ # that always moves forward in time. If that is not available on this
10
+ # system, `Time.now` will be used
11
+ #
12
+ # @return [Float] The current time as a float
13
+ def now
14
+ Process.clock_gettime(Process::CLOCK_MONOTONIC_RAW)
15
+ end
16
+ elsif defined?(Process::CLOCK_MONOTONIC)
17
+ def now
18
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
19
+ end
20
+ elsif Gem.loaded_specs['concurrent-ruby']
21
+ require 'concurrent-ruby'
22
+
23
+ def now
24
+ Concurrent.monotonic_time
25
+ end
26
+ else
27
+ def now
28
+ Time.now.to_f
29
+ end
30
+ end
31
+ end
32
+ 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('0.1.0')
6
+ Gem::Version.new('0.4.1')
7
7
  end
8
8
  end
data/lib/cutoff.rb CHANGED
@@ -1,13 +1,17 @@
1
1
  # frozen_string_literal:true
2
2
 
3
- require_relative 'cutoff/version'
4
- require_relative 'cutoff/error'
5
- require_relative 'cutoff/patch'
3
+ require 'cutoff/version'
4
+ require 'cutoff/error'
5
+ require 'cutoff/patch'
6
+ require 'cutoff/timer'
7
+ require 'cutoff/rails'
6
8
 
7
9
  class Cutoff
8
10
  CURRENT_STACK_KEY = 'cutoff_deadline_stack'
9
11
  private_constant :CURRENT_STACK_KEY
10
12
 
13
+ extend Timer
14
+
11
15
  class << self
12
16
  # Get the current {Cutoff} if one is set
13
17
  def current
@@ -85,31 +89,29 @@ class Cutoff
85
89
  cutoff.checkpoint!
86
90
  end
87
91
 
88
- if defined?(Process::CLOCK_MONOTONIC_RAW)
89
- # The current time
90
- #
91
- # If it is available, this will use a monotonic clock. This is a clock
92
- # that always moves forward in time. If that is not available on this
93
- # system, `Time.now` will be used
94
- #
95
- # @return [Float] The current time as a float
96
- def now
97
- Process.clock_gettime(Process::CLOCK_MONOTONIC_RAW)
98
- end
99
- elsif defined?(Process::CLOCK_MONOTONIC)
100
- def now
101
- Process.clock_gettime(Process::CLOCK_MONOTONIC)
102
- end
103
- elsif Gem.loaded_specs['concurrent-ruby']
104
- require 'concurrent-ruby'
105
-
106
- def now
107
- Concurrent.monotonic_time
108
- end
109
- else
110
- def now
111
- Time.now.to_f
112
- end
92
+ # Disable Cutoff globally. Useful for testing and debugging
93
+ #
94
+ # Should not be used in production
95
+ #
96
+ # @return [void]
97
+ def disable!
98
+ @disabled = true
99
+ end
100
+
101
+ # Enable Cutoff globally if it has been disabled
102
+ #
103
+ # Should not be used in production
104
+ #
105
+ # @return [void]
106
+ def enable!
107
+ @disabled = false
108
+ end
109
+
110
+ # True if cutoff was disabled with {#disable!}
111
+ #
112
+ # @return [Boolean] True if disabled
113
+ def disabled?
114
+ @disabled == true
113
115
  end
114
116
  end
115
117
 
@@ -144,6 +146,8 @@ class Cutoff
144
146
  #
145
147
  # @return [Float] The number of seconds
146
148
  def elapsed_seconds
149
+ return 0 if Cutoff.disabled?
150
+
147
151
  Cutoff.now - @start_time
148
152
  end
149
153
 
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: 0.1.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Howard
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-07-20 00:00:00.000000000 Z
11
+ date: 2021-10-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '3.10'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec-rails
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '5.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '5.0'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: rubocop
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -87,13 +101,17 @@ files:
87
101
  - lib/cutoff/error.rb
88
102
  - lib/cutoff/patch.rb
89
103
  - lib/cutoff/patch/mysql2.rb
104
+ - lib/cutoff/patch/net_http.rb
105
+ - lib/cutoff/rails.rb
106
+ - lib/cutoff/rails/controller.rb
107
+ - lib/cutoff/timer.rb
90
108
  - lib/cutoff/version.rb
91
109
  homepage: https://github.com/justinhoward/cutoff
92
110
  licenses:
93
111
  - MIT
94
112
  metadata:
95
113
  changelog_uri: https://github.com/justinhoward/cutoff/blob/master/CHANGELOG.md
96
- documentation_uri: https://www.rubydoc.info/gems/cutoff/0.1.0
114
+ documentation_uri: https://www.rubydoc.info/gems/cutoff/0.4.1
97
115
  post_install_message:
98
116
  rdoc_options: []
99
117
  require_paths: