sidekiq-unique-jobs 7.1.18 → 7.1.19

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of sidekiq-unique-jobs might be problematic. Click here for more details.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1fffa63d97388ac34a983ddeb04fc3d515bc263af1688884826496dfb53d7c10
4
- data.tar.gz: 4ad0f841add9bf76ca2aa9064eee3dcd55d6cf8e7cbf4d225667edcb1d8ea81f
3
+ metadata.gz: 5432e71679519bcfbc3605de7c156e0820cf2c463ee9a5d458c1b59bd7c63fec
4
+ data.tar.gz: ecd548ff2add944949acc25341d92e2f74aee5256978d32ee74180ba1eacbd66
5
5
  SHA512:
6
- metadata.gz: 854f4bfc85b98bfa04951ff3b9d9276f9723a29d3e564df27e05b6f4321f2cad67adcf52cbc0501f1a247515bd23e358fbf4f9c6ba11c9072bb1bdef0cc97e8d
7
- data.tar.gz: 956d56e8fc94184d02bf8b174534d11e6ddfcb48416f5bdf0df0be56474714bcb08d91ada0d41223eca1784db164842d3fa184ecd9b09689904f3f1e253ac7ed
6
+ metadata.gz: 926bde9f4894aba3806723c1d75faffdf8bde86514b37f69b4156ca12b3dd64228d6eb768a03f521d119ebb42f50bcfbe87392d788f54a6e49b233565743dcb4
7
+ data.tar.gz: 28d14de13123d83b6d948d8453a901efe0ae1667ebc0d6a444e702371579528eccb4ac42c298aa2eb425c23b402c155cc58f553def21cba9c7a6d46757a167e2
data/CHANGELOG.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Changelog
2
2
 
3
- ## [Unreleased](https://github.com/mhenrixon/sidekiq-unique-jobs/tree/HEAD)
3
+ ## [v7.1.18](https://github.com/mhenrixon/sidekiq-unique-jobs/tree/v7.1.18) (2022-04-05)
4
4
 
5
- [Full Changelog](https://github.com/mhenrixon/sidekiq-unique-jobs/compare/v7.1.17...HEAD)
5
+ [Full Changelog](https://github.com/mhenrixon/sidekiq-unique-jobs/compare/v7.1.17...v7.1.18)
6
6
 
7
7
  **Implemented enhancements:**
8
8
 
@@ -1,84 +1,357 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "concurrent/version"
4
- require_relative "version_check"
3
+ require "concurrent/collection/copy_on_notify_observer_set"
4
+ require "concurrent/concern/dereferenceable"
5
+ require "concurrent/concern/observable"
6
+ require "concurrent/atomic/atomic_boolean"
7
+ require "concurrent/executor/executor_service"
8
+ require "concurrent/executor/ruby_executor_service"
9
+ require "concurrent/executor/safe_task_executor"
10
+ require "concurrent/scheduled_task"
5
11
 
6
12
  module SidekiqUniqueJobs
7
- # @see [Concurrent::TimerTask] https://www.rubydoc.info/gems/concurrent-ruby/Concurrent/TimerTask
13
+ # A very common concurrency pattern is to run a thread that performs a task at
14
+ # regular intervals. The thread that performs the task sleeps for the given
15
+ # interval then wakes up and performs the task. Lather, rinse, repeat... This
16
+ # pattern causes two problems. First, it is difficult to test the business
17
+ # logic of the task because the task itself is tightly coupled with the
18
+ # concurrency logic. Second, an exception raised while performing the task can
19
+ # cause the entire thread to abend. In a long-running application where the
20
+ # task thread is intended to run for days/weeks/years a crashed task thread
21
+ # can pose a significant problem. `TimerTask` alleviates both problems.
8
22
  #
9
- class TimerTask < ::Concurrent::TimerTask
10
- if VersionCheck.satisfied?(::Concurrent::VERSION, "< 1.1.10")
23
+ # When a `TimerTask` is launched it starts a thread for monitoring the
24
+ # execution interval. The `TimerTask` thread does not perform the task,
25
+ # however. Instead, the TimerTask launches the task on a separate thread.
26
+ # Should the task experience an unrecoverable crash only the task thread will
27
+ # crash. This makes the `TimerTask` very fault tolerant. Additionally, the
28
+ # `TimerTask` thread can respond to the success or failure of the task,
29
+ # performing logging or ancillary operations. `TimerTask` can also be
30
+ # configured with a timeout value allowing it to kill a task that runs too
31
+ # long.
32
+ #
33
+ # One other advantage of `TimerTask` is that it forces the business logic to
34
+ # be completely decoupled from the concurrency logic. The business logic can
35
+ # be tested separately then passed to the `TimerTask` for scheduling and
36
+ # running.
37
+ #
38
+ # In some cases it may be necessary for a `TimerTask` to affect its own
39
+ # execution cycle. To facilitate this, a reference to the TimerTask instance
40
+ # is passed as an argument to the provided block every time the task is
41
+ # executed.
42
+ #
43
+ # The `TimerTask` class includes the `Dereferenceable` mixin module so the
44
+ # result of the last execution is always available via the `#value` method.
45
+ # Dereferencing options can be passed to the `TimerTask` during construction or
46
+ # at any later time using the `#set_deref_options` method.
47
+ #
48
+ # `TimerTask` supports notification through the Ruby standard library
49
+ # {http://ruby-doc.org/stdlib-2.0/libdoc/observer/rdoc/Observable.html
50
+ # Observable} module. On execution the `TimerTask` will notify the observers
51
+ # with three arguments: time of execution, the result of the block (or nil on
52
+ # failure), and any raised exceptions (or nil on success). If the timeout
53
+ # interval is exceeded the observer will receive a `Concurrent::TimeoutError`
54
+ # object as the third argument.
55
+ #
56
+ # @!macro copy_options
57
+ #
58
+ # @example Basic usage
59
+ # task = Concurrent::TimerTask.new{ puts 'Boom!' }
60
+ # task.execute
61
+ #
62
+ # task.execution_interval #=> 60 (default)
63
+ # task.timeout_interval #=> 30 (default)
64
+ #
65
+ # # wait 60 seconds...
66
+ # #=> 'Boom!'
67
+ #
68
+ # task.shutdown #=> true
69
+ #
70
+ # @example Configuring `:execution_interval` and `:timeout_interval`
71
+ # task = Concurrent::TimerTask.new(execution_interval: 5, timeout_interval: 5) do
72
+ # puts 'Boom!'
73
+ # end
74
+ #
75
+ # task.execution_interval #=> 5
76
+ # task.timeout_interval #=> 5
77
+ #
78
+ # @example Immediate execution with `:run_now`
79
+ # task = Concurrent::TimerTask.new(run_now: true){ puts 'Boom!' }
80
+ # task.execute
81
+ #
82
+ # #=> 'Boom!'
83
+ #
84
+ # @example Last `#value` and `Dereferenceable` mixin
85
+ # task = Concurrent::TimerTask.new(
86
+ # dup_on_deref: true,
87
+ # execution_interval: 5
88
+ # ){ Time.now }
89
+ #
90
+ # task.execute
91
+ # Time.now #=> 2013-11-07 18:06:50 -0500
92
+ # sleep(10)
93
+ # task.value #=> 2013-11-07 18:06:55 -0500
94
+ #
95
+ # @example Controlling execution from within the block
96
+ # timer_task = Concurrent::TimerTask.new(execution_interval: 1) do |task|
97
+ # task.execution_interval.times{ print 'Boom! ' }
98
+ # print "\n"
99
+ # task.execution_interval += 1
100
+ # if task.execution_interval > 5
101
+ # puts 'Stopping...'
102
+ # task.shutdown
103
+ # end
104
+ # end
105
+ #
106
+ # timer_task.execute # blocking call - this task will stop itself
107
+ # #=> Boom!
108
+ # #=> Boom! Boom!
109
+ # #=> Boom! Boom! Boom!
110
+ # #=> Boom! Boom! Boom! Boom!
111
+ # #=> Boom! Boom! Boom! Boom! Boom!
112
+ # #=> Stopping...
113
+ #
114
+ # @example Observation
115
+ # class TaskObserver
116
+ # def update(time, result, ex)
117
+ # if result
118
+ # print "(#{time}) Execution successfully returned #{result}\n"
119
+ # elsif ex.is_a?(Concurrent::TimeoutError)
120
+ # print "(#{time}) Execution timed out\n"
121
+ # else
122
+ # print "(#{time}) Execution failed with error #{ex}\n"
123
+ # end
124
+ # end
125
+ # end
126
+ #
127
+ # task = Concurrent::TimerTask.new(execution_interval: 1, timeout_interval: 1){ 42 }
128
+ # task.add_observer(TaskObserver.new)
129
+ # task.execute
130
+ # sleep 4
131
+ #
132
+ # #=> (2013-10-13 19:08:58 -0400) Execution successfully returned 42
133
+ # #=> (2013-10-13 19:08:59 -0400) Execution successfully returned 42
134
+ # #=> (2013-10-13 19:09:00 -0400) Execution successfully returned 42
135
+ # task.shutdown
136
+ #
137
+ # task = Concurrent::TimerTask.new(execution_interval: 1, timeout_interval: 1){ sleep }
138
+ # task.add_observer(TaskObserver.new)
139
+ # task.execute
140
+ #
141
+ # #=> (2013-10-13 19:07:25 -0400) Execution timed out
142
+ # #=> (2013-10-13 19:07:27 -0400) Execution timed out
143
+ # #=> (2013-10-13 19:07:29 -0400) Execution timed out
144
+ # task.shutdown
145
+ #
146
+ # task = Concurrent::TimerTask.new(execution_interval: 1){ raise StandardError }
147
+ # task.add_observer(TaskObserver.new)
148
+ # task.execute
149
+ #
150
+ # #=> (2013-10-13 19:09:37 -0400) Execution failed with error StandardError
151
+ # #=> (2013-10-13 19:09:38 -0400) Execution failed with error StandardError
152
+ # #=> (2013-10-13 19:09:39 -0400) Execution failed with error StandardError
153
+ # task.shutdown
154
+ #
155
+ # @see http://ruby-doc.org/stdlib-2.0/libdoc/observer/rdoc/Observable.html
156
+ # @see http://docs.oracle.com/javase/7/docs/api/java/util/TimerTask.html
157
+ class TimerTask < Concurrent::RubyExecutorService # rubocop:disable Metrics/ClassLength
158
+ include Concurrent::Concern::Dereferenceable
159
+ include Concurrent::Concern::Observable
11
160
 
12
- private
161
+ # Default `:execution_interval` in seconds.
162
+ EXECUTION_INTERVAL = 60
13
163
 
14
- def ns_initialize(opts, &task)
15
- set_deref_options(opts)
164
+ # Default `:timeout_interval` in seconds.
165
+ TIMEOUT_INTERVAL = 30
16
166
 
17
- self.execution_interval = opts[:execution] || opts[:execution_interval] || EXECUTION_INTERVAL
18
- self.timeout_interval = opts[:timeout] || opts[:timeout_interval] || TIMEOUT_INTERVAL
19
- @run_now = opts[:now] || opts[:run_now]
20
- @executor = Concurrent::RubySingleThreadExecutor.new
21
- @running = Concurrent::AtomicBoolean.new(false)
22
- @task = task
23
- @value = nil
167
+ # Create a new TimerTask with the given task and configuration.
168
+ #
169
+ # @!macro timer_task_initialize
170
+ # @param [Hash] opts the options defining task execution.
171
+ # @option opts [Integer] :execution_interval number of seconds between
172
+ # task executions (default: EXECUTION_INTERVAL)
173
+ # @option opts [Integer] :timeout_interval number of seconds a task can
174
+ # run before it is considered to have failed (default: TIMEOUT_INTERVAL)
175
+ # @option opts [Boolean] :run_now Whether to run the task immediately
176
+ # upon instantiation or to wait until the first # execution_interval
177
+ # has passed (default: false)
178
+ #
179
+ # @!macro deref_options
180
+ #
181
+ # @raise ArgumentError when no block is given.
182
+ #
183
+ # @yield to the block after :execution_interval seconds have passed since
184
+ # the last yield
185
+ # @yieldparam task a reference to the `TimerTask` instance so that the
186
+ # block can control its own lifecycle. Necessary since `self` will
187
+ # refer to the execution context of the block rather than the running
188
+ # `TimerTask`.
189
+ #
190
+ # @return [TimerTask] the new `TimerTask`
191
+ def initialize(opts = {}, &task)
192
+ raise ArgumentError, "no block given" unless task
24
193
 
25
- self.observers = Concurrent::Collection::CopyOnNotifyObserverSet.new
26
- end
194
+ super
195
+ set_deref_options opts
196
+ end
27
197
 
28
- def schedule_next_task(interval = execution_interval)
29
- exec_task = ->(completion) { execute_task(completion) }
30
- Concurrent::ScheduledTask.execute(interval, args: [Concurrent::Event.new], &exec_task)
31
- nil
32
- end
198
+ # Is the executor running?
199
+ #
200
+ # @return [Boolean] `true` when running, `false` when shutting down or shutdown
201
+ def running?
202
+ @running.true?
203
+ end
33
204
 
34
- # @!visibility private
35
- def execute_task(completion) # rubocop:disable Metrics/MethodLength
36
- return nil unless @running.true?
37
-
38
- timeout_task = -> { timeout_task(completion) }
39
-
40
- Concurrent::ScheduledTask.execute(
41
- timeout_interval,
42
- args: [completion],
43
- &timeout_task
44
- )
45
- @thread_completed = Concurrent::Event.new
46
-
47
- @value = @reason = nil
48
- @executor.post do
49
- @value = @task.call(self)
50
- rescue Exception => ex # rubocop:disable Lint/RescueException
51
- @reason = ex
52
- ensure
53
- @thread_completed.set
205
+ # Execute a previously created `TimerTask`.
206
+ #
207
+ # @return [TimerTask] a reference to `self`
208
+ #
209
+ # @example Instance and execute in separate steps
210
+ # task = Concurrent::TimerTask.new(execution_interval: 10){ print "Hello World\n" }
211
+ # task.running? #=> false
212
+ # task.execute
213
+ # task.running? #=> true
214
+ #
215
+ # @example Instance and execute in one line
216
+ # task = Concurrent::TimerTask.new(execution_interval: 10){ print "Hello World\n" }.execute
217
+ # task.running? #=> true
218
+ def execute
219
+ synchronize do
220
+ if @running.false?
221
+ @running.make_true
222
+ schedule_next_task(@run_now ? 0 : @execution_interval)
54
223
  end
224
+ end
225
+ self
226
+ end
227
+
228
+ # Create and execute a new `TimerTask`.
229
+ #
230
+ # @!macro timer_task_initialize
231
+ #
232
+ # @example
233
+ # task = Concurrent::TimerTask.execute(execution_interval: 10){ print "Hello World\n" }
234
+ # task.running? #=> true
235
+ def self.execute(opts = {}, &task)
236
+ SidekiqUniqueJobs::TimerTask.new(opts, &task).execute
237
+ end
55
238
 
56
- @thread_completed.wait
239
+ # @!attribute [rw] execution_interval
240
+ # @return [Fixnum] Number of seconds after the task completes before the
241
+ # task is performed again.
242
+ def execution_interval
243
+ synchronize { @execution_interval }
244
+ end
57
245
 
58
- if completion.try?
59
- schedule_next_task
60
- time = Time.now
61
- observers.notify_observers do
62
- [time, value, @reason]
63
- end
64
- end
65
- nil
66
- end
246
+ # @!attribute [rw] execution_interval
247
+ # @return [Fixnum] Number of seconds after the task completes before the
248
+ # task is performed again.
249
+ def execution_interval=(value)
250
+ raise ArgumentError, "must be greater than zero" if (value = value.to_f) <= 0.0
251
+
252
+ synchronize { @execution_interval = value }
253
+ end
254
+
255
+ # @!attribute [rw] timeout_interval
256
+ # @return [Fixnum] Number of seconds the task can run before it is
257
+ # considered to have failed.
258
+ def timeout_interval
259
+ synchronize { @timeout_interval }
260
+ end
261
+
262
+ # @!attribute [rw] timeout_interval
263
+ # @return [Fixnum] Number of seconds the task can run before it is
264
+ # considered to have failed.
265
+ def timeout_interval=(value)
266
+ raise ArgumentError, "must be greater than zero" if (value = value.to_f) <= 0.0
267
+
268
+ synchronize { @timeout_interval = value }
269
+ end
270
+
271
+ private :post, :<<
272
+
273
+ private
274
+
275
+ def ns_initialize(opts, &task)
276
+ set_deref_options(opts)
277
+
278
+ self.execution_interval = opts[:execution] || opts[:execution_interval] || EXECUTION_INTERVAL
279
+ self.timeout_interval = opts[:timeout] || opts[:timeout_interval] || TIMEOUT_INTERVAL
280
+ @run_now = opts[:now] || opts[:run_now]
281
+ @executor = Concurrent::RubySingleThreadExecutor.new
282
+ @running = Concurrent::AtomicBoolean.new(false)
283
+ @task = task
284
+ @value = nil
285
+
286
+ self.observers = Concurrent::Collection::CopyOnNotifyObserverSet.new
287
+ end
288
+
289
+ # @!visibility private
290
+ def ns_shutdown_execution
291
+ @running.make_false
292
+ super
293
+ end
294
+
295
+ # @!visibility private
296
+ def ns_kill_execution
297
+ @running.make_false
298
+ super
299
+ end
300
+
301
+ # @!visibility private
302
+ def schedule_next_task(interval = execution_interval)
303
+ exec_task = ->(completion) { execute_task(completion) }
304
+ Concurrent::ScheduledTask.execute(interval, args: [Concurrent::Event.new], &exec_task)
305
+ nil
306
+ end
307
+
308
+ # @!visibility private
309
+ def execute_task(completion) # rubocop:disable Metrics/MethodLength
310
+ return nil unless @running.true?
67
311
 
68
- # @!visibility private
69
- def timeout_task(completion)
70
- return unless @running.true?
71
- return unless completion.try?
312
+ timeout_task = -> { timeout_task(completion) }
72
313
 
73
- @executor.kill
74
- @executor.wait_for_termination
75
- @executor = Concurrent::RubySingleThreadExecutor.new
314
+ Concurrent::ScheduledTask.execute(
315
+ timeout_interval,
316
+ args: [completion],
317
+ &timeout_task
318
+ )
319
+ @thread_completed = Concurrent::Event.new
76
320
 
321
+ @value = @reason = nil
322
+ @executor.post do
323
+ @value = @task.call(self)
324
+ rescue Exception => ex # rubocop:disable Lint/RescueException
325
+ @reason = ex
326
+ ensure
77
327
  @thread_completed.set
328
+ end
329
+
330
+ @thread_completed.wait
78
331
 
332
+ if completion.try?
79
333
  schedule_next_task
80
- observers.notify_observers(Time.now, nil, Concurrent::TimeoutError.new)
334
+ time = Time.now
335
+ observers.notify_observers do
336
+ [time, value, @reason]
337
+ end
81
338
  end
339
+ nil
340
+ end
341
+
342
+ # @!visibility private
343
+ def timeout_task(completion)
344
+ return unless @running.true?
345
+ return unless completion.try?
346
+
347
+ @executor.kill
348
+ @executor.wait_for_termination
349
+ @executor = Concurrent::RubySingleThreadExecutor.new
350
+
351
+ @thread_completed.set
352
+
353
+ schedule_next_task
354
+ observers.notify_observers(Time.now, nil, Concurrent::TimeoutError.new)
82
355
  end
83
356
  end
84
357
  end
@@ -3,5 +3,5 @@
3
3
  module SidekiqUniqueJobs
4
4
  #
5
5
  # @return [String] the current SidekiqUniqueJobs version
6
- VERSION = "7.1.18"
6
+ VERSION = "7.1.19"
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sidekiq-unique-jobs
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.1.18
4
+ version: 7.1.19
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-04-05 00:00:00.000000000 Z
11
+ date: 2022-04-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: brpoplpush-redis_script