cutoff 0.4.0 → 0.5.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 +4 -4
- data/CHANGELOG.md +33 -1
- data/README.md +43 -0
- data/lib/cutoff/error.rb +4 -2
- data/lib/cutoff/patch/mysql2.rb +4 -3
- data/lib/cutoff/patch/net_http.rb +28 -16
- data/lib/cutoff/rails.rb +0 -4
- data/lib/cutoff/sidekiq.rb +40 -0
- data/lib/cutoff/timer.rb +8 -9
- data/lib/cutoff/version.rb +1 -1
- data/lib/cutoff.rb +53 -14
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d13746c455ac05f8491ccd8e5f8834370c0e4d41b77b440a2ee33f7c9bafb9db
|
4
|
+
data.tar.gz: 883490b2c6661605d81cf299f809175cadfd1e51b4c68bb218dfa8f996af23cc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e544dff1eb63defd93d8f1f260c12e843bb3178a1a523d63b327ea76ddea533bcd6ce7c68c2b697d859ad795ad62f8048549ae82997c0d6c9edbee92089de6c0
|
7
|
+
data.tar.gz: a1612206aaf0400aa50988b32b35fd7e27418d047e5aab6d7919dfedd30efeaa3675e97f9b6134233f13a54794c01ff7d8868ac892ebf711fc42ef2a65388de5
|
data/CHANGELOG.md
CHANGED
@@ -7,13 +7,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
7
7
|
|
8
8
|
## [Unreleased]
|
9
9
|
|
10
|
+
## [0.5.0] - 2022-08-10
|
11
|
+
|
12
|
+
### Changed
|
13
|
+
|
14
|
+
- Use CLOCK_MONOTONIC instead of CLOCK_MONOTONIC_RAW #10 justinhoward
|
15
|
+
- Change CutoffExceededError to inherit from Timeout::Error #9 justinhoward
|
16
|
+
|
17
|
+
### Breaking
|
18
|
+
|
19
|
+
PR #9 changes the parent class of `Cutoff::CutoffExceededError` from `CutoffError`
|
20
|
+
to `Timeout::Error`. `CutoffError` changes from a class to a module.
|
21
|
+
|
22
|
+
## [0.4.2] - 2021-10-14
|
23
|
+
|
24
|
+
### Added
|
25
|
+
|
26
|
+
- Add sidekiq middleware
|
27
|
+
- Select checkpoints to enable or disable
|
28
|
+
|
29
|
+
## [0.4.1] - 2021-10-02
|
30
|
+
|
31
|
+
### Fixed
|
32
|
+
|
33
|
+
- Fix Net::HTTP patch to override timeouts given to start
|
34
|
+
|
10
35
|
## [0.4.0] - 2021-10-01
|
11
36
|
|
37
|
+
### Added
|
38
|
+
|
12
39
|
- Add benchmarks and slight performance improvements
|
13
40
|
- Add Rails controller integration
|
14
41
|
|
15
42
|
## [0.3.0] - 2021-08-20
|
16
43
|
|
44
|
+
### Added
|
45
|
+
|
17
46
|
- Allow timers to be disabled globally with `Cutoff.disable!`
|
18
47
|
|
19
48
|
## [0.2.0] - 2021-07-22
|
@@ -29,7 +58,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
29
58
|
- Cutoff class
|
30
59
|
- Mysql2 patch
|
31
60
|
|
32
|
-
[Unreleased]: https://github.com/justinhoward/cutoff/compare/v0.
|
61
|
+
[Unreleased]: https://github.com/justinhoward/cutoff/compare/v0.5.0...HEAD
|
62
|
+
[0.5.0]: https://github.com/justinhoward/cutoff/compare/v0.4.2...v0.5.0
|
63
|
+
[0.4.2]: https://github.com/justinhoward/cutoff/compare/v0.4.1...v0.4.2
|
64
|
+
[0.4.1]: https://github.com/justinhoward/cutoff/compare/v0.4.0...v0.4.1
|
33
65
|
[0.4.0]: https://github.com/justinhoward/cutoff/compare/v0.3.0...v0.4.0
|
34
66
|
[0.3.0]: https://github.com/justinhoward/cutoff/compare/v0.2.0...v0.3.0
|
35
67
|
[0.2.0]: https://github.com/justinhoward/cutoff/compare/v0.1.0...v0.2.0
|
data/README.md
CHANGED
@@ -217,6 +217,29 @@ Cutoff.wrap(3) do
|
|
217
217
|
end
|
218
218
|
```
|
219
219
|
|
220
|
+
Selecting Checkpoints
|
221
|
+
---------------------------
|
222
|
+
|
223
|
+
In some cases, you may want to select some checkpoints to use, but not others.
|
224
|
+
For example, you may want to run some code that contains MySQL queries, but not
|
225
|
+
use the mysql2 patch. The `exclude` and `only` options support this.
|
226
|
+
|
227
|
+
```ruby
|
228
|
+
Cutoff.wrap(10, exclude: :mysql2) do
|
229
|
+
# The mysql2 patch won't be used here
|
230
|
+
end
|
231
|
+
|
232
|
+
Cutoff.wrap(10, only: %i[foo bar]) do
|
233
|
+
# These checkpoints will be used
|
234
|
+
Cutoff.checkpoint!(:foo)
|
235
|
+
Cutoff.checkpoint!(:bar)
|
236
|
+
|
237
|
+
# These checkpoints will be skipped
|
238
|
+
Cutoff.checkpoint!(:asdf)
|
239
|
+
Cutoff.checkpoint!
|
240
|
+
end
|
241
|
+
```
|
242
|
+
|
220
243
|
Timing a Rails Controller
|
221
244
|
---------------------------
|
222
245
|
|
@@ -277,6 +300,26 @@ class ApplicationController < ActionController::Base
|
|
277
300
|
end
|
278
301
|
```
|
279
302
|
|
303
|
+
Timing Sidekiq Workers
|
304
|
+
------------
|
305
|
+
|
306
|
+
If Sidekiq is loaded, Cutoff includes middleware to support a `:cutoff` option.
|
307
|
+
|
308
|
+
```ruby
|
309
|
+
class MyWorker
|
310
|
+
include Sidekiq::Worker
|
311
|
+
|
312
|
+
sidekiq_options cutoff: 6.0
|
313
|
+
|
314
|
+
def perform
|
315
|
+
# ...
|
316
|
+
Cutoff.checkpoint!
|
317
|
+
# ...
|
318
|
+
end
|
319
|
+
end
|
320
|
+
```
|
321
|
+
|
322
|
+
|
280
323
|
Disabling Cutoff for Testing and Development
|
281
324
|
------------
|
282
325
|
|
data/lib/cutoff/error.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
class Cutoff
|
4
4
|
# The Cutoff base error class
|
5
|
-
|
5
|
+
module CutoffError
|
6
6
|
private
|
7
7
|
|
8
8
|
def message_with_meta(message, **meta)
|
@@ -15,7 +15,9 @@ class Cutoff
|
|
15
15
|
end
|
16
16
|
|
17
17
|
# Raised by {Cutoff#checkpoint!} if the time has been exceeded
|
18
|
-
class CutoffExceededError <
|
18
|
+
class CutoffExceededError < Timeout::Error
|
19
|
+
include CutoffError
|
20
|
+
|
19
21
|
attr_reader :cutoff
|
20
22
|
|
21
23
|
def initialize(cutoff)
|
data/lib/cutoff/patch/mysql2.rb
CHANGED
@@ -6,7 +6,8 @@ require 'mysql2'
|
|
6
6
|
class Cutoff
|
7
7
|
module Patch
|
8
8
|
# Sets the max execution time for SELECT queries if there is an active
|
9
|
-
# cutoff and it has time remaining
|
9
|
+
# cutoff and it has time remaining. You can select this patch with
|
10
|
+
# `exclude` or `only` using the checkpoint name `:mysql2`.
|
10
11
|
module Mysql2
|
11
12
|
# Overrides `Mysql2::Client#query` to insert a MAX_EXECUTION_TIME query
|
12
13
|
# hint with the remaining cutoff time
|
@@ -19,9 +20,9 @@ class Cutoff
|
|
19
20
|
# be executed in this case.
|
20
21
|
def query(sql, options = {})
|
21
22
|
cutoff = Cutoff.current
|
22
|
-
return super unless cutoff
|
23
|
+
return super unless cutoff&.selected?(:mysql2)
|
23
24
|
|
24
|
-
cutoff.checkpoint!
|
25
|
+
cutoff.checkpoint!(:mysql2)
|
25
26
|
sql = QueryWithMaxTime.new(sql, cutoff.ms_remaining.ceil).to_s
|
26
27
|
super
|
27
28
|
end
|
@@ -4,27 +4,39 @@ require 'net/http'
|
|
4
4
|
|
5
5
|
class Cutoff
|
6
6
|
module Patch
|
7
|
-
#
|
8
|
-
# to the remaining time
|
7
|
+
# Set checkpoints for Ruby HTTP requests. Also sets the Net::HTTP timeouts
|
8
|
+
# to the remaining cutoff time. You can select this patch with
|
9
|
+
# `exclude` or `only` using the checkpoint name `:net_http`.
|
9
10
|
module NetHttp
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
11
|
+
def self.gen_timeout_method(name)
|
12
|
+
<<~RUBY
|
13
|
+
if #{name}.nil? || #{name} > remaining
|
14
|
+
self.#{name} = cutoff.seconds_remaining
|
15
|
+
end
|
16
|
+
RUBY
|
17
|
+
end
|
15
18
|
|
16
|
-
|
17
|
-
|
18
|
-
@write_timeout = cutoff.seconds_remaining
|
19
|
+
def self.use_write_timeout?
|
20
|
+
Gem::Version.new(RUBY_VERSION) > Gem::Version.new('2.6')
|
19
21
|
end
|
20
22
|
|
21
|
-
# Same as the original start, but
|
23
|
+
# Same as the original start, but adds a checkpoint for starting HTTP
|
24
|
+
# requests and sets network timeouts to the remaining time
|
22
25
|
#
|
23
|
-
# @see
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
26
|
+
# @see Net::HTTP#start
|
27
|
+
module_eval(<<~RUBY, __FILE__, __LINE__ + 1)
|
28
|
+
def start
|
29
|
+
if (cutoff = Cutoff.current) && cutoff.selected?(:net_http)
|
30
|
+
remaining = cutoff.seconds_remaining
|
31
|
+
#{gen_timeout_method('open_timeout')}
|
32
|
+
#{gen_timeout_method('read_timeout')}
|
33
|
+
#{gen_timeout_method('write_timeout') if use_write_timeout?}
|
34
|
+
#{gen_timeout_method('continue_timeout')}
|
35
|
+
Cutoff.checkpoint!(:net_http)
|
36
|
+
end
|
37
|
+
super
|
38
|
+
end
|
39
|
+
RUBY
|
28
40
|
end
|
29
41
|
end
|
30
42
|
end
|
data/lib/cutoff/rails.rb
CHANGED
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sidekiq'
|
4
|
+
|
5
|
+
class Cutoff
|
6
|
+
module Sidekiq
|
7
|
+
# Add an option `cutoff` for sidekiq workers
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# class MyWorker
|
11
|
+
# include Sidekiq::Worker
|
12
|
+
#
|
13
|
+
# sidekiq_options cutoff: 6.0
|
14
|
+
#
|
15
|
+
# def perform
|
16
|
+
# # ...
|
17
|
+
# end
|
18
|
+
# end
|
19
|
+
class ServerMiddleware
|
20
|
+
# @param worker [Object] the worker instance
|
21
|
+
# @param _job [Hash] the full job payload
|
22
|
+
# @param _queue [String] queue the name of the queue the job was pulled
|
23
|
+
# from
|
24
|
+
# @yield the next middleware in the chain or worker `perform` method
|
25
|
+
# @return [void]
|
26
|
+
def call(worker, _job, _queue)
|
27
|
+
allowed_seconds = worker.class.sidekiq_options['cutoff']
|
28
|
+
return yield if allowed_seconds.nil?
|
29
|
+
|
30
|
+
Cutoff.wrap(allowed_seconds) { yield }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
::Sidekiq.configure_server do |config|
|
37
|
+
config.server_middleware do |chain|
|
38
|
+
chain.add(Cutoff::Sidekiq::ServerMiddleware)
|
39
|
+
end
|
40
|
+
end
|
data/lib/cutoff/timer.rb
CHANGED
@@ -2,18 +2,17 @@
|
|
2
2
|
|
3
3
|
class Cutoff
|
4
4
|
module Timer
|
5
|
-
if defined?(Process::
|
6
|
-
# The current time
|
5
|
+
if defined?(Process::CLOCK_MONOTONIC)
|
6
|
+
# The current relative time
|
7
7
|
#
|
8
8
|
# If it is available, this will use a monotonic clock. This is a clock
|
9
|
-
# that always moves forward in time
|
10
|
-
# system
|
9
|
+
# that always moves forward in time and starts at an arbitrary point
|
10
|
+
# (such as system startup time). If that is not available on this system,
|
11
|
+
# `Time.now` will be used.
|
11
12
|
#
|
12
|
-
#
|
13
|
-
|
14
|
-
|
15
|
-
end
|
16
|
-
elsif defined?(Process::CLOCK_MONOTONIC)
|
13
|
+
# This does not represent current real time
|
14
|
+
#
|
15
|
+
# @return [Float] The current relative time as a float
|
17
16
|
def now
|
18
17
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
19
18
|
end
|
data/lib/cutoff/version.rb
CHANGED
data/lib/cutoff.rb
CHANGED
@@ -1,10 +1,15 @@
|
|
1
1
|
# frozen_string_literal:true
|
2
2
|
|
3
|
+
require 'set'
|
4
|
+
require 'timeout'
|
5
|
+
|
3
6
|
require 'cutoff/version'
|
4
7
|
require 'cutoff/error'
|
5
8
|
require 'cutoff/patch'
|
6
9
|
require 'cutoff/timer'
|
10
|
+
|
7
11
|
require 'cutoff/rails'
|
12
|
+
require 'cutoff/sidekiq' if Gem.loaded_specs['sidekiq']
|
8
13
|
|
9
14
|
class Cutoff
|
10
15
|
CURRENT_STACK_KEY = 'cutoff_deadline_stack'
|
@@ -25,13 +30,14 @@ class Cutoff
|
|
25
30
|
# If a cutoff is already started for this thread, then `start` uses the
|
26
31
|
# minimum of the current remaining time and the given time
|
27
32
|
#
|
28
|
-
# @param
|
29
|
-
# be overridden if there is an active cutoff and it has less remaining
|
30
|
-
# time.
|
33
|
+
# @param (see #initialize)
|
31
34
|
# @return [Cutoff] The {Cutoff} instance
|
32
|
-
def start(
|
33
|
-
|
34
|
-
|
35
|
+
def start(allowed_seconds, **options)
|
36
|
+
if current
|
37
|
+
allowed_seconds = [allowed_seconds, current.seconds_remaining].min
|
38
|
+
end
|
39
|
+
|
40
|
+
cutoff = new(allowed_seconds, **options)
|
35
41
|
Thread.current[CURRENT_STACK_KEY] ||= []
|
36
42
|
Thread.current[CURRENT_STACK_KEY] << cutoff
|
37
43
|
cutoff
|
@@ -68,9 +74,10 @@ class Cutoff
|
|
68
74
|
#
|
69
75
|
# @see .start
|
70
76
|
# @see .stop
|
77
|
+
# @param (see #initialize)
|
71
78
|
# @return The value that returned from the block
|
72
|
-
def wrap(
|
73
|
-
cutoff = start(
|
79
|
+
def wrap(allowed_seconds, **options)
|
80
|
+
cutoff = start(allowed_seconds, **options)
|
74
81
|
yield cutoff
|
75
82
|
ensure
|
76
83
|
stop(cutoff)
|
@@ -82,11 +89,11 @@ class Cutoff
|
|
82
89
|
#
|
83
90
|
# @raise CutoffExceededError If there is an active expired cutoff
|
84
91
|
# @return [void]
|
85
|
-
def checkpoint!
|
92
|
+
def checkpoint!(name = nil)
|
86
93
|
cutoff = current
|
87
94
|
return unless cutoff
|
88
95
|
|
89
|
-
cutoff.checkpoint!
|
96
|
+
cutoff.checkpoint!(name)
|
90
97
|
end
|
91
98
|
|
92
99
|
# Disable Cutoff globally. Useful for testing and debugging
|
@@ -107,7 +114,7 @@ class Cutoff
|
|
107
114
|
@disabled = false
|
108
115
|
end
|
109
116
|
|
110
|
-
# True if cutoff was disabled with {
|
117
|
+
# True if cutoff was disabled with {.disable!}
|
111
118
|
#
|
112
119
|
# @return [Boolean] True if disabled
|
113
120
|
def disabled?
|
@@ -122,10 +129,22 @@ class Cutoff
|
|
122
129
|
#
|
123
130
|
# The timer starts immediately upon creation
|
124
131
|
#
|
125
|
-
# @param allowed_seconds [
|
126
|
-
|
132
|
+
# @param allowed_seconds [Float, Integer] The total number of seconds to allow
|
133
|
+
# @param exclude [Enumberable<Symbol>, Symbol, nil] If given a name or
|
134
|
+
# list of checkpoint names to skip
|
135
|
+
# @param only [Enumberable<Symbol>, Symbol, nil] If given a name or
|
136
|
+
# list of checkpoint names to allow
|
137
|
+
def initialize(allowed_seconds, exclude: nil, only: nil)
|
127
138
|
@allowed_seconds = allowed_seconds.to_f
|
128
139
|
@start_time = Cutoff.now
|
140
|
+
|
141
|
+
if exclude
|
142
|
+
@exclude = Set.new(exclude.is_a?(Enumerable) ? exclude : [exclude])
|
143
|
+
end
|
144
|
+
|
145
|
+
if only
|
146
|
+
@only = Set.new(only.is_a?(Enumerable) ? only : [only])
|
147
|
+
end
|
129
148
|
end
|
130
149
|
|
131
150
|
# The number of seconds left on the clock
|
@@ -162,9 +181,29 @@ class Cutoff
|
|
162
181
|
#
|
163
182
|
# @raise CutoffExceededError If there is an active expired cutoff
|
164
183
|
# @return [void]
|
165
|
-
def checkpoint!
|
184
|
+
def checkpoint!(name = nil)
|
185
|
+
unless name.nil? || name.is_a?(Symbol)
|
186
|
+
raise ArgumentError, 'name must be a symbol'
|
187
|
+
end
|
188
|
+
|
189
|
+
return unless selected?(name)
|
166
190
|
raise CutoffExceededError, self if exceeded?
|
167
191
|
|
168
192
|
nil
|
169
193
|
end
|
194
|
+
|
195
|
+
# True if the named checkpoint is selected by the `exclude` and `only`
|
196
|
+
# options. If these options are not given, a checkpoint is considered to be
|
197
|
+
# selected. If the checkpoint is not named, it is also considered to be
|
198
|
+
# selected.
|
199
|
+
#
|
200
|
+
# @param name [Symbol, nil] The name of the checkpoint
|
201
|
+
# @return [Boolean] True if the checkpoint is selected
|
202
|
+
def selected?(name)
|
203
|
+
return true if name.nil? && @exclude
|
204
|
+
return false if @exclude&.include?(name)
|
205
|
+
return false if @only && !@only.include?(name)
|
206
|
+
|
207
|
+
true
|
208
|
+
end
|
170
209
|
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: 0.
|
4
|
+
version: 0.5.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:
|
11
|
+
date: 2022-08-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec
|
@@ -104,6 +104,7 @@ files:
|
|
104
104
|
- lib/cutoff/patch/net_http.rb
|
105
105
|
- lib/cutoff/rails.rb
|
106
106
|
- lib/cutoff/rails/controller.rb
|
107
|
+
- lib/cutoff/sidekiq.rb
|
107
108
|
- lib/cutoff/timer.rb
|
108
109
|
- lib/cutoff/version.rb
|
109
110
|
homepage: https://github.com/justinhoward/cutoff
|
@@ -111,7 +112,7 @@ licenses:
|
|
111
112
|
- MIT
|
112
113
|
metadata:
|
113
114
|
changelog_uri: https://github.com/justinhoward/cutoff/blob/master/CHANGELOG.md
|
114
|
-
documentation_uri: https://www.rubydoc.info/gems/cutoff/0.
|
115
|
+
documentation_uri: https://www.rubydoc.info/gems/cutoff/0.5.0
|
115
116
|
post_install_message:
|
116
117
|
rdoc_options: []
|
117
118
|
require_paths:
|