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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 630a704243a3bd8c2196e0be8bba735eb8f2afb185c3db7ba68f3c8bed242041
4
- data.tar.gz: 1922fbb0c382c62fd894cd8d2f0bfa2d21ef5bbf2cdb5f7b128d9de582c14344
3
+ metadata.gz: d13746c455ac05f8491ccd8e5f8834370c0e4d41b77b440a2ee33f7c9bafb9db
4
+ data.tar.gz: 883490b2c6661605d81cf299f809175cadfd1e51b4c68bb218dfa8f996af23cc
5
5
  SHA512:
6
- metadata.gz: 7dcb206df1a3dc2ce265e0b78f0d93a8c3ddfe1f92b52945ff06cc87fa20ecfcaf2f4f9636555966b7c5cd9b624a498be788bd8fdf3d6c67cd9c5ffbd35aa393
7
- data.tar.gz: 8178c8dd01267bc67b96eef6b2ff103f90a9e47954c55d10b33f1c21a39f36701bfb2bfd0e6896f6975a210e66c09611cd8cf419d9ea62b2cf489603dcff8808
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.4.0...HEAD
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
- class CutoffError < StandardError
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 < CutoffError
18
+ class CutoffExceededError < Timeout::Error
19
+ include CutoffError
20
+
19
21
  attr_reader :cutoff
20
22
 
21
23
  def initialize(cutoff)
@@ -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
- # Adds a checkpoint for starting HTTP requests and sets network timeouts
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
- # Construct a {Net::HTTP}, but with the timeouts set to the remaining
11
- # cutoff time if one is active
12
- def initialize(address, port = nil)
13
- super
14
- return unless (cutoff = Cutoff.current)
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
- @open_timeout = cutoff.seconds_remaining
17
- @read_timeout = cutoff.seconds_remaining
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 with a cutoff checkpoint
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 {Net::HTTP#start}
24
- def start
25
- Cutoff.checkpoint!
26
- super
27
- end
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
@@ -1,7 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Namespace for Rails integration
4
- module Rails
5
- end
6
-
7
3
  require 'cutoff/rails/controller' if Gem.loaded_specs['actionpack']
@@ -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::CLOCK_MONOTONIC_RAW)
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. If that is not available on this
10
- # system, `Time.now` will be used
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
- # @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)
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
@@ -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.4.0')
6
+ Gem::Version.new('0.5.0')
7
7
  end
8
8
  end
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 seconds [Float, Integer] The number of seconds for the cutoff. May
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(seconds)
33
- seconds = [seconds, current.seconds_remaining].min if current
34
- cutoff = Cutoff.new(seconds)
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(seconds)
73
- cutoff = start(seconds)
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 {#disable!}
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 [Integer, Float] The total number of seconds to allow
126
- def initialize(allowed_seconds)
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.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: 2021-10-02 00:00:00.000000000 Z
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.4.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: