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 +4 -4
- data/CHANGELOG.md +24 -0
- data/README.md +96 -20
- data/lib/cutoff/patch/mysql2.rb +3 -3
- data/lib/cutoff/patch/net_http.rb +41 -0
- data/lib/cutoff/rails/controller.rb +48 -0
- data/lib/cutoff/rails.rb +7 -0
- data/lib/cutoff/timer.rb +32 -0
- data/lib/cutoff/version.rb +1 -1
- data/lib/cutoff.rb +32 -28
- metadata +21 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5ac27ce016591276e55afa82f64c08de370a969cb6552cc0d5ae69093d3a9e0c
|
4
|
+
data.tar.gz: 31e989979ae04de967a22e8d2457d6da654862603f380dd7521e415c9220148b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
24
|
-
|
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
|
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.
|
142
|
-
|
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
|
-
|
156
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
200
|
-
#
|
201
|
-
|
250
|
+
class UsersController < ApplicationController
|
251
|
+
# For example, use an :only filter
|
252
|
+
cutoff 5.0, only: :index
|
202
253
|
|
203
|
-
#
|
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
|
364
|
-
|
365
|
-
|
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.
|
data/lib/cutoff/patch/mysql2.rb
CHANGED
@@ -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
|
49
|
-
handle_token(@scanner[1]
|
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.
|
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)
|
data/lib/cutoff/rails.rb
ADDED
data/lib/cutoff/timer.rb
ADDED
@@ -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
|
data/lib/cutoff/version.rb
CHANGED
data/lib/cutoff.rb
CHANGED
@@ -1,13 +1,17 @@
|
|
1
1
|
# frozen_string_literal:true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
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
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
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
|
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-
|
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
|
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:
|