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 +4 -4
- data/CHANGELOG.md +17 -0
- data/README.md +61 -16
- data/VERSION +1 -1
- data/lib/sidekiq/deferred_jobs.rb +129 -48
- data/lib/sidekiq/deferred_jobs_ext.rb +4 -1
- data/sidekiq-deferred_jobs.gemspec +0 -2
- metadata +1 -15
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f46a91476e205cb7c8ac96647c7b138f5a525ac1e1e1a8a70578835b6d946850
|
|
4
|
+
data.tar.gz: 4e0b11956ca657075e5dde2de0fb31764c6e9063d88be065528c587fb35e4579
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
[](https://github.com/testdouble/standard)
|
|
5
5
|
[](https://badge.fury.io/rb/sidekiq-deferred_jobs)
|
|
6
6
|
|
|
7
|
-
This gem provides an enhancement to [Sidekiq](https://github.com/
|
|
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
|
|
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/
|
|
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
|
|
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`
|
|
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::
|
|
44
|
+
include Sidekiq::Job
|
|
45
45
|
sidekiq_options priority: "high"
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
class OtherWorker
|
|
49
|
-
include Sidekiq::
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
110
|
+
# SomeWorker.perform_async(2) is not yet enqueued
|
|
93
111
|
|
|
94
112
|
Sidekiq.enqueue_deferred_jobs!
|
|
95
|
-
# SomeWorker.
|
|
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
|
|
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
|
-
|
|
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.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
|
|
15
|
-
# @return [
|
|
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(
|
|
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 [
|
|
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]
|
|
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
|
|
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
|
|
84
|
-
# have sidekiq_options that match the specified hash.
|
|
85
|
-
#
|
|
86
|
-
#
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
292
|
+
unmatched_jobs = []
|
|
293
|
+
pending_jobs = @jobs
|
|
204
294
|
begin
|
|
205
|
-
|
|
206
|
-
|
|
295
|
+
until pending_jobs.empty?
|
|
296
|
+
job = pending_jobs.first
|
|
207
297
|
if filter.match?(job.klass, job.opts)
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
306
|
+
unmatched_jobs << job
|
|
219
307
|
end
|
|
308
|
+
pending_jobs.shift
|
|
220
309
|
end
|
|
221
310
|
ensure
|
|
222
|
-
@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
|
|
229
|
-
def unique_job?(
|
|
230
|
-
enterprise_option =
|
|
231
|
-
unique_jobs_option =
|
|
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
|
-
|
|
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
|
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.
|
|
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: []
|