cutoff 0.4.1 → 0.5.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: 5ac27ce016591276e55afa82f64c08de370a969cb6552cc0d5ae69093d3a9e0c
4
- data.tar.gz: 31e989979ae04de967a22e8d2457d6da654862603f380dd7521e415c9220148b
3
+ metadata.gz: 7ab1dba25fcad8b3f65d816d1740dbd2c2e1b6102f4009e19fa36fd0baf8236c
4
+ data.tar.gz: 7a86b6dbf5c0d0bd6631281687c674ec21dc19102097bee351b8e13a3b2a53c3
5
5
  SHA512:
6
- metadata.gz: 4241c63fc4c647c44860aa0781e2966a5eadc1d7edea259851018308d1877d664c46a8d90bd5553f725d2e1fa5f767995b0f27327482bf2ce5128701908aa684
7
- data.tar.gz: 64d599559055f6d8b3c384b88b5268b02317784bd44fd6be2384fd3c56b1f9da5a9cfb435c4af05c083ba20af6d8865f0c73a8fc46afff261ad70d43fc9c35e4
6
+ metadata.gz: e23dcd3d2fa5b44bcc3630aa4976cff12eef9771f4c1010a2cd12cc820ef5caabf1e54b0dba83de60af1ce2142eea4d53bae7c79e5c02ac34ec91d1e07cfba62
7
+ data.tar.gz: bfc01027499784eaeaf89f5bc4114483bf89bc41eb2db6668a297ace172b58136ae58b214df6f0929a027729aff417ff56b9fb4dfacd45cb979d280b675f3868
data/.yardopts CHANGED
@@ -1,3 +1,4 @@
1
1
  --markup markdown
2
2
  --markup-provider redcarpet
3
3
  --no-private
4
+ --no-api
data/CHANGELOG.md CHANGED
@@ -7,17 +7,54 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.5.1] - 2022-09-06
11
+
12
+ ### Changed
13
+
14
+ - Upgrade rubocop to latest version #12 justinhoward
15
+ - Add codecov.io code coverage #11 justinhoward
16
+ - Complete yard documentation and fix warnings #13 justinhoward
17
+
18
+ ### Fixed
19
+
20
+ - Pull cutoff option from job, not worker #14 mperham
21
+
22
+ ## [0.5.0] - 2022-08-10
23
+
24
+ ### Changed
25
+
26
+ - Use CLOCK_MONOTONIC instead of CLOCK_MONOTONIC_RAW #10 justinhoward
27
+ - Change CutoffExceededError to inherit from Timeout::Error #9 justinhoward
28
+
29
+ ### Breaking
30
+
31
+ PR #9 changes the parent class of `Cutoff::CutoffExceededError` from `CutoffError`
32
+ to `Timeout::Error`. `CutoffError` changes from a class to a module.
33
+
34
+ ## [0.4.2] - 2021-10-14
35
+
36
+ ### Added
37
+
38
+ - Add sidekiq middleware
39
+ - Select checkpoints to enable or disable
40
+
10
41
  ## [0.4.1] - 2021-10-02
11
42
 
43
+ ### Fixed
44
+
12
45
  - Fix Net::HTTP patch to override timeouts given to start
13
46
 
14
47
  ## [0.4.0] - 2021-10-01
15
48
 
49
+ ### Added
50
+
16
51
  - Add benchmarks and slight performance improvements
17
52
  - Add Rails controller integration
18
53
 
19
54
  ## [0.3.0] - 2021-08-20
20
55
 
56
+ ### Added
57
+
21
58
  - Allow timers to be disabled globally with `Cutoff.disable!`
22
59
 
23
60
  ## [0.2.0] - 2021-07-22
@@ -33,8 +70,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
33
70
  - Cutoff class
34
71
  - Mysql2 patch
35
72
 
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
73
+ [Unreleased]: https://github.com/justinhoward/cutoff/compare/v0.5.1...HEAD
74
+ [0.5.1]: https://github.com/justinhoward/cutoff/compare/v0.5.0...v0.5.1
75
+ [0.5.0]: https://github.com/justinhoward/cutoff/compare/v0.4.2...v0.5.0
76
+ [0.4.2]: https://github.com/justinhoward/cutoff/compare/v0.4.1...v0.4.2
77
+ [0.4.1]: https://github.com/justinhoward/cutoff/compare/v0.4.0...v0.4.1
38
78
  [0.4.0]: https://github.com/justinhoward/cutoff/compare/v0.3.0...v0.4.0
39
79
  [0.3.0]: https://github.com/justinhoward/cutoff/compare/v0.2.0...v0.3.0
40
80
  [0.2.0]: https://github.com/justinhoward/cutoff/compare/v0.1.0...v0.2.0
data/README.md CHANGED
@@ -4,7 +4,7 @@ Cutoff
4
4
  [![Gem Version](https://badge.fury.io/rb/cutoff.svg)](https://badge.fury.io/rb/cutoff)
5
5
  [![CI](https://github.com/justinhoward/cutoff/workflows/CI/badge.svg)](https://github.com/justinhoward/cutoff/actions?query=workflow%3ACI+branch%3Amaster)
6
6
  [![Code Quality](https://app.codacy.com/project/badge/Grade/2748da79ec294f909996a56f11caac4a)](https://www.codacy.com/gh/justinhoward/cutoff/dashboard?utm_source=github.com&utm_medium=referral&utm_content=justinhoward/cutoff&utm_campaign=Badge_Grade)
7
- [![Code Coverage](https://app.codacy.com/project/badge/Coverage/2748da79ec294f909996a56f11caac4a)](https://www.codacy.com/gh/justinhoward/cutoff/dashboard?utm_source=github.com&utm_medium=referral&utm_content=justinhoward/cutoff&utm_campaign=Badge_Coverage)
7
+ [![Code Coverage](https://codecov.io/gh/justinhoward/cutoff/branch/master/graph/badge.svg?token=COVM3D2PTG)](https://codecov.io/gh/justinhoward/cutoff)
8
8
  [![Inline docs](http://inch-ci.org/github/justinhoward/cutoff.svg?branch=master)](http://inch-ci.org/github/justinhoward/cutoff)
9
9
 
10
10
  A deadlines library for Ruby inspired by Shopify and
@@ -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
@@ -166,4 +167,9 @@ class Cutoff
166
167
  end
167
168
  end
168
169
 
169
- Mysql2::Client.prepend(Cutoff::Patch::Mysql2)
170
+ # @api external
171
+ module Mysql2
172
+ class Client
173
+ prepend Cutoff::Patch::Mysql2
174
+ end
175
+ end
@@ -4,32 +4,40 @@ require 'net/http'
4
4
 
5
5
  class Cutoff
6
6
  module Patch
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`.
7
10
  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
11
+ class << self
12
+ private
13
+
14
+ def gen_timeout_method(name)
15
+ <<~RUBY
16
+ if #{name}.nil? || #{name} > remaining
17
+ self.#{name} = cutoff.seconds_remaining
18
+ end
19
+ RUBY
20
+ end
15
21
 
16
- def self.use_write_timeout?
17
- Gem::Version.new(RUBY_VERSION) > Gem::Version.new('2.6')
22
+ def use_write_timeout?
23
+ Gem::Version.new(RUBY_VERSION) > Gem::Version.new('2.6')
24
+ end
18
25
  end
19
26
 
20
27
  # Same as the original start, but adds a checkpoint for starting HTTP
21
28
  # requests and sets network timeouts to the remaining time
22
29
  #
30
+ # @method start
23
31
  # @see Net::HTTP#start
24
32
  module_eval(<<~RUBY, __FILE__, __LINE__ + 1)
25
33
  def start
26
- if (cutoff = Cutoff.current)
34
+ if (cutoff = Cutoff.current) && cutoff.selected?(:net_http)
27
35
  remaining = cutoff.seconds_remaining
28
36
  #{gen_timeout_method('open_timeout')}
29
37
  #{gen_timeout_method('read_timeout')}
30
38
  #{gen_timeout_method('write_timeout') if use_write_timeout?}
31
39
  #{gen_timeout_method('continue_timeout')}
32
- Cutoff.checkpoint!
40
+ Cutoff.checkpoint!(:net_http)
33
41
  end
34
42
  super
35
43
  end
@@ -38,4 +46,9 @@ class Cutoff
38
46
  end
39
47
  end
40
48
 
41
- Net::HTTP.prepend(Cutoff::Patch::NetHttp)
49
+ # @api external
50
+ module Net
51
+ class HTTP
52
+ prepend Cutoff::Patch::NetHttp
53
+ end
54
+ end
@@ -3,6 +3,7 @@
3
3
  require 'action_controller'
4
4
 
5
5
  class Cutoff
6
+ # Cutoff Rails extensions
6
7
  module Rails
7
8
  # Rails controller integration
8
9
  module Controller
@@ -45,4 +46,9 @@ class Cutoff
45
46
  end
46
47
  end
47
48
 
48
- ActionController::Base.extend(Cutoff::Rails::Controller)
49
+ # @api external
50
+ module ActionController
51
+ class Base
52
+ extend Cutoff::Rails::Controller
53
+ end
54
+ 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,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sidekiq'
4
+
5
+ class Cutoff
6
+ # Cutoff sidekiq extensions
7
+ module Sidekiq
8
+ # Add an option `cutoff` for sidekiq workers
9
+ #
10
+ # @example
11
+ # class MyWorker
12
+ # include Sidekiq::Worker
13
+ #
14
+ # sidekiq_options cutoff: 6.0
15
+ #
16
+ # def perform
17
+ # # ...
18
+ # end
19
+ # end
20
+ class ServerMiddleware
21
+ # @param _worker [Object] the worker instance
22
+ # @param job [Hash] the full job payload
23
+ # @param _queue [String] queue the name of the queue the job was pulled
24
+ # from
25
+ # @yield the next middleware in the chain or worker `perform` method
26
+ # @return [void]
27
+ def call(_worker, job, _queue)
28
+ allowed_seconds = job['cutoff']
29
+ return yield if allowed_seconds.nil?
30
+
31
+ Cutoff.wrap(allowed_seconds) { yield }
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ ::Sidekiq.configure_server do |config|
38
+ config.server_middleware do |chain|
39
+ chain.add(Cutoff::Sidekiq::ServerMiddleware)
40
+ end
41
+ end
data/lib/cutoff/timer.rb CHANGED
@@ -1,19 +1,19 @@
1
1
  # frozen_string_literal:true
2
2
 
3
3
  class Cutoff
4
+ # Tracks the current time for cutoff
4
5
  module Timer
5
- if defined?(Process::CLOCK_MONOTONIC_RAW)
6
- # The current time
6
+ if defined?(Process::CLOCK_MONOTONIC)
7
+ # The current relative time
7
8
  #
8
9
  # 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
10
+ # that always moves forward in time and starts at an arbitrary point
11
+ # (such as system startup time). If that is not available on this system,
12
+ # `Time.now` will be used.
11
13
  #
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)
14
+ # This does not represent current real time
15
+ #
16
+ # @return [Float] The current relative time as a float
17
17
  def now
18
18
  Process.clock_gettime(Process::CLOCK_MONOTONIC)
19
19
  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.1')
6
+ Gem::Version.new('0.5.1')
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.1
4
+ version: 0.5.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-10-02 00:00:00.000000000 Z
11
+ date: 2022-09-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -38,34 +38,6 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '5.0'
41
- - !ruby/object:Gem::Dependency
42
- name: rubocop
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - '='
46
- - !ruby/object:Gem::Version
47
- version: 0.81.0
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - '='
53
- - !ruby/object:Gem::Version
54
- version: 0.81.0
55
- - !ruby/object:Gem::Dependency
56
- name: rubocop-rspec
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - '='
60
- - !ruby/object:Gem::Version
61
- version: 1.38.1
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - '='
67
- - !ruby/object:Gem::Version
68
- version: 1.38.1
69
41
  - !ruby/object:Gem::Dependency
70
42
  name: timecop
71
43
  requirement: !ruby/object:Gem::Requirement
@@ -104,6 +76,7 @@ files:
104
76
  - lib/cutoff/patch/net_http.rb
105
77
  - lib/cutoff/rails.rb
106
78
  - lib/cutoff/rails/controller.rb
79
+ - lib/cutoff/sidekiq.rb
107
80
  - lib/cutoff/timer.rb
108
81
  - lib/cutoff/version.rb
109
82
  homepage: https://github.com/justinhoward/cutoff
@@ -111,7 +84,8 @@ licenses:
111
84
  - MIT
112
85
  metadata:
113
86
  changelog_uri: https://github.com/justinhoward/cutoff/blob/master/CHANGELOG.md
114
- documentation_uri: https://www.rubydoc.info/gems/cutoff/0.4.1
87
+ documentation_uri: https://www.rubydoc.info/gems/cutoff/0.5.1
88
+ rubygems_mfa_required: 'true'
115
89
  post_install_message:
116
90
  rdoc_options: []
117
91
  require_paths: