sidekiq-deferred_jobs 1.0.1 → 1.0.2

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: e4232cb827ec1a6bd2f218f34ac5b7f3a0b175a0a4dd34841633605d669bfb16
4
- data.tar.gz: 3db11095242ca521612cae0b91e1b20abf5fb0676b5fa8f75d0220ab9fa8e8c6
3
+ metadata.gz: f46a91476e205cb7c8ac96647c7b138f5a525ac1e1e1a8a70578835b6d946850
4
+ data.tar.gz: 4e0b11956ca657075e5dde2de0fb31764c6e9063d88be065528c587fb35e4579
5
5
  SHA512:
6
- metadata.gz: 2d8f9ebdad90e5641e6d96c8356ce83e9aad680203f51eb3f0ca933a5d2384f5a52c29d55af0b1fb51ee5e67de9e9a51e085408ce2e56be90b834d95ffdf7279
7
- data.tar.gz: 6ee5fc3e3a76a7cd4e7fd96d642fab6664a831d5801240fb0614fe03738f43c0ab6108628478191a9f9a3cd231b7d6dffeb4e9c2d25ba76cd917c19b14223090
6
+ metadata.gz: 7e2e71026882f25f043d43710ddc228c1a8aa74f984621b3a9a62ab5297a13289a93e583958ef5379ed5a9b05c10752041815e21627a3705e52b3997664ee180
7
+ data.tar.gz: ef95d50cad3d5ab1e21e5beb912eed08c39d774f856df4105c8db41a828957b1258d3a5bdb2ca7d0d0240bf849fc39ad2a37c9da2b73fdd808c4f66b7425af2c
data/CHANGELOG.md CHANGED
@@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## 1.0.2
8
+
9
+ ### Fixed
10
+
11
+ - Scheduled jobs enqueued through a `Setter` (i.e. `MyWorker.set(queue: "high").perform_in(60)` or `MyWorker.set(wait: 60).perform_async`) are no longer deferred. This matches the existing behavior of `MyWorker.perform_in` since scheduled jobs are not enqueued to run immediately anyway.
12
+ - Added missing `require "set"` which could cause a `NameError` when deferred jobs were enqueued on Ruby < 3.2 in applications that did not already load the set library.
13
+ - Deferred jobs that have not yet been enqueued are no longer silently dropped if an error is raised while enqueuing jobs. The error is still raised to the calling code, but the remaining jobs are retained so `Sidekiq.enqueue_deferred_jobs!` can be safely retried.
14
+ - Duplicate unique jobs are no longer suppressed if they are enqueued to different queues.
15
+ - Runtime options are no longer lost when a job is passed through `Sidekiq::DeferredJobs.defer_worker` outside of a defer block.
16
+ - Fixed a `NameError` on application boot with Sidekiq 5.0 through 6.2 when `sidekiq/api` was loaded before this gem. Those versions define an unrelated `Sidekiq::Job` class in the API which was mistaken for the `Sidekiq::Job` mixin added in Sidekiq 6.3.
17
+ - Jobs enqueued with `set(sync: true)` are no longer deferred; they run inline at the call site as Sidekiq intends.
18
+ - A hash filter passed directly to `Sidekiq::DeferredJobs.defer` is now applied as a `sidekiq_options` filter. Previously it silently matched all jobs.
19
+
20
+ ### Changed
21
+
22
+ - Filters passed to `Sidekiq.defer_jobs`, `Sidekiq.abort_deferred_jobs!`, and `Sidekiq.enqueue_deferred_jobs!` now raise an `ArgumentError` if they are not classes, modules, or hashes. Previously invalid filter values would silently match all jobs.
23
+
7
24
  ## 1.0.1
8
25
 
9
26
  ### Changed
data/README.md CHANGED
@@ -4,13 +4,13 @@
4
4
  [![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)
5
5
  [![Gem Version](https://badge.fury.io/rb/sidekiq-deferred_jobs.svg)](https://badge.fury.io/rb/sidekiq-deferred_jobs)
6
6
 
7
- This gem provides an enhancement to [Sidekiq](https://github.com/mperham/sidekiq) to defer enqueuing jobs until the end of a block of code. This is useful in situations where you need to better coordinate when jobs are enqueued to guard against race conditions or deduplicate jobs. In most cases, this provides no functional difference to your code; it just delays slightly when jobs are enqueued.
7
+ This gem provides an enhancement to [Sidekiq](https://github.com/sidekiq/sidekiq) to defer enqueuing jobs until the end of a block of code. This is useful in situations where you need to better coordinate when jobs are enqueued to guard against race conditions or deduplicate jobs. In most cases, this provides no functional difference to your code; it just delays slightly when jobs are enqueued.
8
8
 
9
9
  ## Usage
10
10
 
11
- If you have a complex operation composed of several discrete service objects that each fire off Sidekiq jobs, but you need to coordinate when those jobs are actually run, you could use this code to do so. This might be to avoid race condition where you don't want some jobs running until the entire operation is finished or because some of the code fires off duplicate jobs that you'd like to squash. If you have a worker that automatically fires on data updates to send synchronization messages to another systems, you might want to have only a single job run at the end of the all the updates rather than sending multiple updates within a few milliseconds. This gem is designed to give you control over that situation rather than having to refactor code that may have side effects in other situations.
11
+ If you have a complex operation composed of several discrete service objects that each fire off Sidekiq jobs, but you need to coordinate when those jobs are actually run, you could use this code to do so. This might be to avoid a race condition where you don't want some jobs running until the entire operation is finished, or because some of the code fires off duplicate jobs that you'd like to squash. If you have a worker that automatically fires on data updates to send synchronization messages to other systems, you might want to have only a single job run at the end of all the updates rather than sending multiple updates within a few milliseconds. This gem is designed to give you control over that situation rather than having to refactor code that may have side effects in other situations.
12
12
 
13
- If you are using either [the sidekiq-unique-jobs gem](https://github.com/mhenrixon/sidekiq-unique-jobs) or [Sidekiq Enterprise unique jobs](https://github.com/mperham/sidekiq/wiki/Ent-Unique-Jobs), then unique jobs equeued in the deferred jobs block will be suppressed. This is useful since Sidekiq can be so fast that duplicate jobs can be picked up by worker threads almost instataneously so the system never detects that duplicate jobs were being enqueued.
13
+ If you are using either [the sidekiq-unique-jobs gem](https://github.com/mhenrixon/sidekiq-unique-jobs) or [Sidekiq Enterprise unique jobs](https://github.com/sidekiq/sidekiq/wiki/Ent-Unique-Jobs), then duplicate jobs deferred within the block will be collapsed into a single job when the block exits. A job is considered a duplicate if it has the same class, arguments, and queue as a job already enqueued by the same flush of deferred jobs. The collapse does not read the uniqueness mechanism's own lock configuration (i.e. `unique_args`, `lock_args`, or `unique_across_queues`); jobs that are duplicates only under those settings are all enqueued and the uniqueness mechanism resolves them when they are pushed. When jobs are collapsed, the first job and its runtime options are kept. This is useful since Sidekiq can be so fast that duplicate jobs can be picked up by worker threads almost instantaneously, so the system never detects that duplicate jobs were being enqueued. Jobs that do not declare a uniqueness constraint (`unique_for` for Sidekiq Enterprise, or `lock` for sidekiq-unique-jobs) are never collapsed; neither are jobs locked only `while_executing`.
14
14
 
15
15
  Using the scheduled jobs mechanism in Sidekiq to accomplish the same thing is less than ideal because the scheduling mechanism in Sidekiq is not designed to be very precise. If you schedule a job to run one second in the future, it might not run for several seconds.
16
16
 
@@ -35,18 +35,18 @@ end
35
35
  # All the jobs are now enqueued
36
36
  ```
37
37
 
38
- The workers are fired in an `ensure` block, so even if an error is raised, any jobs that would have been enqueued prior to the error will still be enqueued.
38
+ The deferred jobs are enqueued from an `ensure` block, so even if an error is raised inside the block, any jobs that would have been enqueued prior to the error will still be enqueued.
39
39
 
40
- You can also pass a filter to `defer_jobs` to filter either by class or by `sidekiq_options`.
40
+ You can also pass a filter to `defer_jobs` so that only some jobs are deferred. A filter can be a class, a module, or a hash of `sidekiq_options`. A job matches a class or module filter if the worker class is that class, a subclass of it, or includes that module. It matches a hash filter if all of the entries in the hash match the worker's `sidekiq_options` (including any options set at runtime with `set`). You can pass multiple filters; a job is deferred if it matches any of them.
41
41
 
42
42
  ```ruby
43
43
  class SomeWorker
44
- include Sidekiq::Worker
44
+ include Sidekiq::Job
45
45
  sidekiq_options priority: "high"
46
46
  end
47
47
 
48
48
  class OtherWorker
49
- include Sidekiq::Worker
49
+ include Sidekiq::Job
50
50
  end
51
51
 
52
52
  # Filter by worker class
@@ -70,35 +70,71 @@ end
70
70
  # The SomeWorker job will now be enqueued
71
71
  ```
72
72
 
73
- You can also pass `false` to `Sidekiq.defer_jobs` turn off deferral entirely within a block.
73
+ Blocks can be nested. Deferred jobs are always enqueued at the end of the outermost block, and the filters from all of the enclosing blocks apply, so a job is deferred if it matches the filter on any block it is inside of.
74
74
 
75
75
  ```ruby
76
- Sidekiq.defer_jobs(false) do
76
+ Sidekiq.defer_jobs(SomeWorker) do
77
+ Sidekiq.defer_jobs(OtherWorker) do
78
+ SomeWorker.perform_async(1) # deferred by the outer block's filter
79
+ OtherWorker.perform_async(2) # deferred by the inner block's filter
80
+ end
81
+ # Neither job is enqueued here; the inner block does not flush jobs.
82
+ end
83
+ # Both jobs are now enqueued
84
+ ```
85
+
86
+ You can pass `false` to `Sidekiq.defer_jobs` to turn off deferral entirely within a block. This is mostly useful inside a `defer_jobs` block to opt a section of code back into enqueuing jobs immediately.
87
+
88
+ ```ruby
89
+ Sidekiq.defer_jobs do
77
90
  SomeWorker.perform_async(1)
78
- # The SomeWorker job will be enqueued
91
+ # The SomeWorker job is deferred
92
+
93
+ Sidekiq.defer_jobs(false) do
94
+ OtherWorker.perform_async(2)
95
+ # The OtherWorker job is enqueued immediately
96
+ end
79
97
  end
80
98
  ```
81
99
 
82
- You can also manually control over when deferred jobs are enqueued or even remove previously deferred jobs.
100
+ You can also manually control when deferred jobs are enqueued or even remove previously deferred jobs.
83
101
 
84
102
  ```ruby
85
103
  Sidekiq.defer_jobs do
86
104
  SomeWorker.perform_async(1)
87
105
 
88
- # This will cancel SomeWorker.perform(1); it won't be enqueued
106
+ # This will cancel SomeWorker.perform_async(1); it won't be enqueued
89
107
  Sidekiq.abort_deferred_jobs!
90
108
 
91
109
  SomeWorker.perform_async(2)
92
- # SomeWorker.perform(2) is not yet equeued
110
+ # SomeWorker.perform_async(2) is not yet enqueued
93
111
 
94
112
  Sidekiq.enqueue_deferred_jobs!
95
- # SomeWorker.perform(2) will now be be equeued
113
+ # SomeWorker.perform_async(2) has now been enqueued
96
114
  end
97
115
  ```
98
116
 
99
- You can pass filters to the `Sidekiq.abort_deferred_jobs!` and `Sidekiq.enqueue_deferred_jobs!` methods if you want to enqueue or abort just specific jobs. These filters work the same as the fitlers to `Sidekiq.defer_jobs`.
117
+ You can pass filters to the `Sidekiq.abort_deferred_jobs!` and `Sidekiq.enqueue_deferred_jobs!` methods if you want to enqueue or abort just specific jobs. These filters work the same as the class, module, and hash filters for `Sidekiq.defer_jobs` (`false` is not supported here). `Sidekiq.abort_deferred_jobs!` returns the jobs it removed; both methods do nothing when called outside of a `defer_jobs` block.
118
+
119
+ Scheduled jobs (i.e. `perform_in`, `perform_at`, or setting the `at` option with `set`) are never deferred since they are not enqueued to run immediately anyway.
100
120
 
101
- Note that if you are running with a relational database you may want to use another mechanism to work with transactional data (i.e. the `after_commit` hook in ActiveRecord). However, if you have a single logical operation that contains multiple transactions, this mechanism could be a good fit. For example, if you have a complex business operation that updates multiple rows and calls external services, you may not want a single transaction since it could lock database rows for a long period creating performance problems. This gem could be used to orchestrate transactional logic for Sidekiq workers in systems with native transaction support.
121
+ If an error is raised while deferred jobs are being enqueued (for instance, if Redis is briefly unavailable), the error will be raised to the calling code. Any jobs that had not yet been enqueued when the error occurred are retained in the deferred jobs list rather than being silently dropped. If the error came from an explicit call to `Sidekiq.enqueue_deferred_jobs!`, you can rescue it and call the method again to enqueue the remaining jobs without duplicating the ones that already made it to Redis.
122
+
123
+ ## Limitations
124
+
125
+ - Only jobs enqueued with `perform_async` are deferred. Jobs enqueued through other mechanisms (`perform_bulk`, `Sidekiq::Client.push`, or ActiveJob with the Sidekiq adapter) are not intercepted and will be enqueued immediately.
126
+
127
+ - `perform_async` does not return a job id for a deferred job. The job id is not assigned until the job is actually enqueued, so any code that captures the return value of `perform_async` should not be run inside a `defer_jobs` block.
128
+
129
+ - The list of deferred jobs is stored in fiber-local storage. Jobs enqueued from other threads or fibers spawned inside a `defer_jobs` block (including code using lazy enumerators or async frameworks that switch fibers) will not be deferred.
130
+
131
+ - If an error is raised by the automatic flush at the end of the outermost `defer_jobs` block, any jobs that had not been enqueued yet are lost; there is no way to retry them since the deferred job list is no longer reachable at that point.
132
+
133
+ > [!IMPORTANT]
134
+ > Deferred jobs are not persisted to Redis within the `defer_jobs` block. If your application crashes or is force killed before the block completes, any jobs that were deferred will be lost. This is a tradeoff to avoid the performance overhead of persisting jobs to Redis when they are deferred.
135
+
136
+ > [!NOTE]
137
+ > If you are running with a relational database you may want to use another mechanism to work with transactional data (i.e. the `after_commit` hook in ActiveRecord). However, if you have a single logical operation that contains multiple transactions, this mechanism could be a good fit. For example, if you have a complex business operation that updates multiple rows and calls external services, you may not want a single transaction since it could lock database rows for a long period creating performance problems. This gem could be used to orchestrate transactional logic for Sidekiq workers in systems with native transaction support.
102
138
 
103
139
  ## Installation
104
140
 
@@ -118,6 +154,15 @@ Or install it yourself as:
118
154
  $ gem install sidekiq-deferred_jobs
119
155
  ```
120
156
 
157
+ No further setup is required; requiring the gem patches Sidekiq automatically.
158
+
159
+ ### Requirements
160
+
161
+ - Ruby 2.5 or later
162
+ - Sidekiq 5.0 or later
163
+
164
+ The examples in this README use `Sidekiq::Job`, which was added in Sidekiq 6.3. If you are on an older version of Sidekiq, use `Sidekiq::Worker` instead.
165
+
121
166
  ## Contributing
122
167
 
123
168
  Open a pull request on GitHub.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.0.1
1
+ 1.0.2
@@ -1,26 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "set"
3
4
  require "sidekiq"
4
5
 
5
6
  module Sidekiq
7
+ # Adds the ability to defer enqueuing Sidekiq jobs until the end of a block of code.
8
+ # See Sidekiq::DeferredJobs::DeferBlock for the methods that are added to `Sidekiq`
9
+ # itself (`Sidekiq.defer_jobs`, `Sidekiq.abort_deferred_jobs!`, and
10
+ # `Sidekiq.enqueue_deferred_jobs!`).
6
11
  module DeferredJobs
7
12
  class << self
8
13
  # Defer enqueuing Sidekiq workers within the block until the end of the block.
9
14
  # Any workers that normally would have been enqueued with a `perform_async` call
10
15
  # will instead be queued up and run in an ensure clause at the end of the block.
11
16
  #
17
+ # Blocks can be nested. Deferred jobs are only enqueued at the end of the outermost
18
+ # block and the filters from all of the enclosing blocks apply, so a job is deferred
19
+ # if it matches the filter on any block it is nested inside of.
20
+ #
12
21
  # @param filter [Array<Module>, Array<Hash>] An array of either classes, modules, or hashes.
13
22
  # If this is provided, only workers that match either a class or module or which have
14
- # sidekiq_options that match a hash will be deferred. All other worker will be enqueued as normal.
15
- # @return [void]
23
+ # sidekiq_options that match a hash will be deferred. All other workers will be enqueued as normal.
24
+ # @return [Object] the return value of the block
16
25
  def defer(filter, &block)
26
+ new_filter = Filter.new(filter)
17
27
  jobs, filters = Thread.current[:sidekiq_deferred_jobs_jobs]
18
28
  unless jobs
19
29
  filters = []
20
30
  jobs = Jobs.new
21
31
  Thread.current[:sidekiq_deferred_jobs_jobs] = [jobs, filters]
22
32
  end
23
- filters.push(Filter.new(filter))
33
+ filters.push(new_filter)
24
34
  begin
25
35
  yield
26
36
  ensure
@@ -33,9 +43,10 @@ module Sidekiq
33
43
  end
34
44
 
35
45
  # Disable deferred workers within the block. All workers will be enqueued normally
36
- # within the block.
46
+ # within the block. Any jobs already deferred by an enclosing block are left alone
47
+ # and will still be enqueued when that block exits.
37
48
  #
38
- # @return [void]
49
+ # @return [Object] the return value of the block
39
50
  def undeferred(&block)
40
51
  save_val = Thread.current[:sidekiq_deferred_jobs_jobs]
41
52
  begin
@@ -47,17 +58,20 @@ module Sidekiq
47
58
  end
48
59
 
49
60
  # Return true if the specified class with optional options should be deferred.
61
+ # Jobs scheduled to run in the future and jobs that run inline are never deferred.
50
62
  #
51
63
  # @param klass [Class] A Sidekiq worker class
52
- # @param opts [Hash, Nil] Optionsl options set at runtime for the worker.
53
- # @return Boolean
64
+ # @param opts [Hash, Nil] Optional options set at runtime for the worker.
65
+ # @return [Boolean]
54
66
  def defer?(klass, opts = nil)
67
+ return false if scheduled_opts?(opts) || inline_opts?(opts)
55
68
  _jobs, filters = Thread.current[:sidekiq_deferred_jobs_jobs]
56
69
  return false if filters.nil?
57
70
  filters.any? { |filter| filter.match?(klass, opts) }
58
71
  end
59
72
 
60
- # Schedule a worker to be run at the end of the outermost defer block.
73
+ # Schedule a worker to be enqueued at the end of the outermost defer block.
74
+ # If there is no defer block in progress, the worker is enqueued immediately.
61
75
  #
62
76
  # @param klass [Class] Sidekiq worker class
63
77
  # @param args [Array] Sidekiq job arguments
@@ -67,12 +81,64 @@ module Sidekiq
67
81
  jobs, _filters = Thread.current[:sidekiq_deferred_jobs_jobs]
68
82
  if jobs
69
83
  jobs.defer(klass, args, opts)
84
+ else
85
+ enqueue_job(klass, args, opts)
86
+ end
87
+ end
88
+
89
+ # Return true if the options indicate a job scheduled to run in the future.
90
+ # Scheduled jobs are never deferred since they are not enqueued immediately anyway.
91
+ #
92
+ # @param opts [Hash, Nil] Runtime options set for a job.
93
+ # @return [Boolean]
94
+ # @api private
95
+ def scheduled_opts?(opts)
96
+ return false unless opts
97
+ !!(opts["at"] || opts[:at])
98
+ end
99
+
100
+ # Return true if the options indicate a job that runs inline at the call site
101
+ # rather than being pushed to a queue. Inline jobs are never deferred.
102
+ #
103
+ # @param opts [Hash, Nil] Runtime options set for a job.
104
+ # @return [Boolean]
105
+ # @api private
106
+ def inline_opts?(opts)
107
+ return false unless opts
108
+ opts["sync"] == true || opts[:sync] == true
109
+ end
110
+
111
+ # Enqueue a job through the normal Sidekiq mechanism.
112
+ #
113
+ # @param klass [Class] Sidekiq worker class
114
+ # @param args [Array] Sidekiq job arguments
115
+ # @param opts [Hash, Nil] Optional runtime options for the job
116
+ # @return [Object] the value returned by `perform_async`
117
+ # @api private
118
+ def enqueue_job(klass, args, opts = nil)
119
+ if opts
120
+ klass.set(opts).perform_async(*args)
70
121
  else
71
122
  klass.perform_async(*args)
72
123
  end
73
124
  end
125
+
126
+ # Merge runtime options with the worker class sidekiq_options.
127
+ #
128
+ # @param klass [Class] Sidekiq worker class
129
+ # @param opts [Hash, Nil] Optional runtime options for the job
130
+ # @return [Hash] the merged options with string keys
131
+ # @api private
132
+ def worker_options(klass, opts = nil)
133
+ if opts
134
+ klass.sidekiq_options.merge(opts.transform_keys(&:to_s))
135
+ else
136
+ klass.sidekiq_options
137
+ end
138
+ end
74
139
  end
75
140
 
141
+ # Methods added to the `Sidekiq` module itself for controlling deferred jobs.
76
142
  module DeferBlock
77
143
  # Defer enqueuing Sidekiq workers within the block until the end of the block.
78
144
  # Any workers that normally would have been enqueued with a `perform_async` call
@@ -80,10 +146,12 @@ module Sidekiq
80
146
  #
81
147
  # @param filter [Array<Module>, Hash, false] Optional filter on which workers should be deferred.
82
148
  # If a filter is specified, only matching workers will be deferred. To match the
83
- # filter, the worker must either be the class specfied or include the module or
84
- # have sidekiq_options that match the specified hash. If the filter is `false`
85
- # then job deferral will be disabled entirely within the block.
86
- # @return [void]
149
+ # filter, the worker must either be the class specified (or a subclass of it),
150
+ # include the module, or have sidekiq_options that match the specified hash.
151
+ # Multiple filters can be specified and a worker will be deferred if it matches
152
+ # any of them. If the filter is `false` then job deferral will be disabled
153
+ # entirely within the block.
154
+ # @return [Object] the return value of the block
87
155
  def defer_jobs(*filter, &block)
88
156
  if filter.size == 1 && filter.first == false
89
157
  Sidekiq::DeferredJobs.undeferred(&block)
@@ -92,10 +160,12 @@ module Sidekiq
92
160
  end
93
161
  end
94
162
 
95
- # Abort any already deferred Sidkiq workers in the current `defer_job` block.
163
+ # Abort any already deferred Sidekiq workers in the current `defer_jobs` block.
96
164
  # If a filter is specified, then only matching Sidekiq jobs will be aborted.
165
+ # Does nothing if called outside of a `defer_jobs` block.
97
166
  #
98
- # @param filter [Array<Module>, Hash, false] See #defer_job for filter specification.
167
+ # @param filter [Array<Module>, Hash] See #defer_jobs for filter specification.
168
+ # Unlike #defer_jobs, `false` is not a valid filter here.
99
169
  # @return [Array<Sidekiq::DeferredJobs::Job>] the jobs that were aborted
100
170
  def abort_deferred_jobs!(*filter)
101
171
  jobs, _filters = Thread.current[:sidekiq_deferred_jobs_jobs]
@@ -106,10 +176,12 @@ module Sidekiq
106
176
  end
107
177
  end
108
178
 
109
- # Immediately enqueue any already deferred Sidkiq workers in the current `defer_job` block.
179
+ # Immediately enqueue any already deferred Sidekiq workers in the current `defer_jobs` block.
110
180
  # If a filter is specified, then only matching Sidekiq jobs will be enqueued.
181
+ # Does nothing if called outside of a `defer_jobs` block.
111
182
  #
112
- # @param filter [Array<Module>, Hash, false] See #defer_job for filter specification.
183
+ # @param filter [Array<Module>, Hash] See #defer_jobs for filter specification.
184
+ # Unlike #defer_jobs, `false` is not a valid filter here.
113
185
  # @return [void]
114
186
  def enqueue_deferred_jobs!(*filter)
115
187
  jobs, _filters = Thread.current[:sidekiq_deferred_jobs_jobs]
@@ -122,6 +194,8 @@ module Sidekiq
122
194
 
123
195
  # Override logic for Sidekiq::Worker.
124
196
  module DeferredWorker
197
+ # @return [Object] the job id if the job was enqueued. If the job was deferred, the
198
+ # return value is unspecified since no job id exists until the job is enqueued.
125
199
  def perform_async(*args)
126
200
  if Sidekiq::DeferredJobs.defer?(self)
127
201
  Sidekiq::DeferredJobs.defer_worker(self, args)
@@ -133,6 +207,8 @@ module Sidekiq
133
207
 
134
208
  # Override logic for Sidekiq::Worker::Setter.
135
209
  module DeferredSetter
210
+ # @return [Object] the job id if the job was enqueued. If the job was deferred, the
211
+ # return value is unspecified since no job id exists until the job is enqueued.
136
212
  def perform_async(*args)
137
213
  if Sidekiq::DeferredJobs.defer?(@klass, @opts)
138
214
  Sidekiq::DeferredJobs.defer_worker(@klass, args, @opts)
@@ -145,7 +221,11 @@ module Sidekiq
145
221
  # Logic for filtering jobs by worker class and/or sidekiq_options.
146
222
  class Filter
147
223
  def initialize(filters)
148
- @filters = Array(filters).flatten
224
+ @filters = (filters.is_a?(Hash) ? [filters] : Array(filters).flatten)
225
+ invalid = @filters.reject { |filter| filter.is_a?(Module) || filter.is_a?(Hash) }
226
+ unless invalid.empty?
227
+ raise ArgumentError, "Filters must be classes, modules, or hashes; got #{invalid.collect(&:inspect).join(", ")}"
228
+ end
149
229
  end
150
230
 
151
231
  # @return [Boolean] true if the job matches the filters.
@@ -154,11 +234,9 @@ module Sidekiq
154
234
  @filters.any? do |filter|
155
235
  if filter.is_a?(Module)
156
236
  klass <= filter
157
- elsif filter.is_a?(Hash)
158
- worker_options = (opts ? klass.sidekiq_options.merge(opts.transform_keys(&:to_s)) : klass.sidekiq_options)
159
- filter.all? { |key, value| worker_options[key.to_s] == value }
160
237
  else
161
- filter
238
+ worker_options = Sidekiq::DeferredJobs.worker_options(klass, opts)
239
+ filter.all? { |key, value| worker_options[key.to_s] == value }
162
240
  end
163
241
  end
164
242
  end
@@ -171,6 +249,7 @@ module Sidekiq
171
249
  class Jobs
172
250
  def initialize
173
251
  @jobs = []
252
+ @dedup_keys = Set.new
174
253
  end
175
254
 
176
255
  # Add a job to the deferred job list.
@@ -196,39 +275,50 @@ module Sidekiq
196
275
 
197
276
  # Enqueue any deferred jobs that match the filter.
198
277
  #
199
- # @param filters [Array<Module>, Array<Hash>] Filter for jobs to clear
278
+ # If a worker declares a uniqueness constraint (either with Sidekiq Enterprise or the
279
+ # sidekiq-unique-jobs gem), then duplicate jobs with the same class, arguments, and queue
280
+ # are collapsed into a single job within this call.
281
+ #
282
+ # If enqueuing a job raises an error, the error will be propagated to the caller
283
+ # and any jobs that have not yet been enqueued will be retained in the deferred
284
+ # job list so they are not silently lost and can be enqueued again. The keys of
285
+ # unique jobs that were already enqueued are remembered until a call completes
286
+ # without an error so that retrying does not enqueue duplicates.
287
+ #
288
+ # @param filters [Array<Module>, Array<Hash>] Filter for jobs to enqueue
200
289
  # @return [void]
201
290
  def enqueue!(filters = nil)
202
291
  filter = Filter.new(filters)
203
- remaining_jobs = []
292
+ unmatched_jobs = []
293
+ pending_jobs = @jobs
204
294
  begin
205
- duplicates = Set.new
206
- @jobs.each do |job|
295
+ until pending_jobs.empty?
296
+ job = pending_jobs.first
207
297
  if filter.match?(job.klass, job.opts)
208
- if unique_job?(job.klass, job.opts)
209
- next if duplicates.include?([job.klass, job.args])
210
- duplicates << [job.klass, job.args]
211
- end
212
- if job.opts
213
- job.klass.set(job.opts).perform_async(*job.args)
214
- else
215
- job.klass.perform_async(*job.args)
298
+ options = Sidekiq::DeferredJobs.worker_options(job.klass, job.opts)
299
+ dedup_key = nil
300
+ dedup_key = [job.klass, job.args, options["queue"]] if unique_job?(options)
301
+ unless dedup_key && @dedup_keys.include?(dedup_key)
302
+ Sidekiq::DeferredJobs.enqueue_job(job.klass, job.args, job.opts)
216
303
  end
304
+ @dedup_keys << dedup_key if dedup_key
217
305
  else
218
- remaining_jobs << job
306
+ unmatched_jobs << job
219
307
  end
308
+ pending_jobs.shift
220
309
  end
221
310
  ensure
222
- @jobs = remaining_jobs
311
+ @jobs = unmatched_jobs + pending_jobs
312
+ @dedup_keys.clear if pending_jobs.empty?
223
313
  end
224
314
  end
225
315
 
226
316
  private
227
317
 
228
- # @return [Boolean] true if the worker support a uniqueness constraint
229
- def unique_job?(klass, opts)
230
- enterprise_option = worker_options(klass, opts)["unique_for"] if defined?(Sidekiq::Enterprise)
231
- unique_jobs_option = worker_options(klass, opts)["lock"] if defined?(SidekiqUniqueJobs)
318
+ # @return [Boolean] true if the options declare a uniqueness constraint
319
+ def unique_job?(options)
320
+ enterprise_option = options["unique_for"] if defined?(Sidekiq::Enterprise)
321
+ unique_jobs_option = options["lock"] if defined?(SidekiqUniqueJobs)
232
322
 
233
323
  if enterprise_option
234
324
  true
@@ -238,15 +328,6 @@ module Sidekiq
238
328
  false
239
329
  end
240
330
  end
241
-
242
- # Merge runtime options with the worker class sidekiq_options.
243
- def worker_options(klass, opts)
244
- if opts
245
- klass.sidekiq_options.merge(opts.transform_keys(&:to_s))
246
- else
247
- klass.sidekiq_options
248
- end
249
- end
250
331
  end
251
332
  end
252
333
  end
@@ -6,7 +6,10 @@
6
6
 
7
7
  Sidekiq.extend(Sidekiq::DeferredJobs::DeferBlock)
8
8
 
9
- if defined?(Sidekiq::Job)
9
+ # Sidekiq::Job is the job mixin on Sidekiq 6.3+, but older versions define an
10
+ # unrelated Sidekiq::Job class in sidekiq/api.rb, so check for the mixin's
11
+ # ClassMethods constant rather than just the presence of the Sidekiq::Job constant.
12
+ if defined?(Sidekiq::Job) && Sidekiq::Job.const_defined?(:ClassMethods)
10
13
  Sidekiq::Job::ClassMethods.prepend(Sidekiq::DeferredJobs::DeferredWorker)
11
14
  Sidekiq::Job::Setter.prepend(Sidekiq::DeferredJobs::DeferredSetter)
12
15
  else
@@ -35,7 +35,5 @@ Gem::Specification.new do |spec|
35
35
 
36
36
  spec.add_dependency "sidekiq", ">= 5.0"
37
37
 
38
- spec.add_development_dependency "bundler"
39
-
40
38
  spec.required_ruby_version = ">= 2.5"
41
39
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sidekiq-deferred_jobs
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Durand
@@ -23,20 +23,6 @@ dependencies:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
25
  version: '5.0'
26
- - !ruby/object:Gem::Dependency
27
- name: bundler
28
- requirement: !ruby/object:Gem::Requirement
29
- requirements:
30
- - - ">="
31
- - !ruby/object:Gem::Version
32
- version: '0'
33
- type: :development
34
- prerelease: false
35
- version_requirements: !ruby/object:Gem::Requirement
36
- requirements:
37
- - - ">="
38
- - !ruby/object:Gem::Version
39
- version: '0'
40
26
  email:
41
27
  - bbdurand@gmail.com
42
28
  executables: []