cutoff 0.1.0 → 0.4.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: 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: