sidekiq-deferred_jobs 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 74140b3a7726b81f99deb8eee4f6b12cbe306a89fc47ee7c1f9c1498b6b39588
4
+ data.tar.gz: aefb420df0176f907074fdbce0a1a6c40f85209b5143ae41342f2cc78ddcf66a
5
+ SHA512:
6
+ metadata.gz: fd4ef0a5ead779291359d18c8388ddc5fa1ee8a5615bf4e1d11add7f0ed605919979380cba966ed8c8422e35d03e4d5910470c405145f31c1f5d299a7e07747e
7
+ data.tar.gz: f1ea50c549baea07d8b8388455c948d052060ee7e68effe12e85e261aa1b6c330702819b84c42ecfa4d11b95a4c7af74bf031d154a372f097461342b6c3f002a
data/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## 1.0.0
8
+
9
+ ### Added
10
+ - Add behavior to Sidekiq to allow deferring enqueing jobs until after a block of code finishes.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2021 Brian Durand
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,130 @@
1
+ [![Continuous Integration](https://github.com/bdurand/sidekiq-deferred_jobs/actions/workflows/continuous_integration.yml/badge.svg)](https://github.com/bdurand/sidekiq-deferred_jobs/actions/workflows/continuous_integration.yml)
2
+ [![Maintainability](https://api.codeclimate.com/v1/badges/3f5fb49ca1d03f698d5b/maintainability)](https://codeclimate.com/github/bdurand/sidekiq-deferred_jobs/maintainability)
3
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/3f5fb49ca1d03f698d5b/test_coverage)](https://codeclimate.com/github/bdurand/sidekiq-deferred_jobs/test_coverage)
4
+ [![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)
5
+
6
+ # Sidekiq Deferred Jobs
7
+
8
+ 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.
9
+
10
+ ## Usage
11
+
12
+ 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.
13
+
14
+ 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.
15
+
16
+ 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.
17
+
18
+ Normally, Sidekiq will enqueue jobs immediately.
19
+
20
+ ```ruby
21
+ ids.each do |id|
22
+ SomeWorker.perform_async(id)
23
+ # Each SomeWorker job is enqueued here
24
+ end
25
+ ```
26
+
27
+ Calling `Sidekiq.defer_jobs` with a block prevents any workers from being immediately enqueued within the block.
28
+
29
+ ```ruby
30
+ Sidekiq.defer_jobs do
31
+ ids.each do |id|
32
+ SomeWorker.perform_async(id)
33
+ # SomeWorker jobs will not be enqueued here
34
+ end
35
+ end
36
+ # All the jobs are now enqueued
37
+ ```
38
+
39
+ 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.
40
+
41
+ You can also pass a filter to `defer_jobs` to filter either by class or by `sidekiq_options`.
42
+
43
+ ```ruby
44
+ class SomeWorker
45
+ include Sidekiq::Worker
46
+ sidekiq_options priority: "high"
47
+ end
48
+
49
+ class OtherWorker
50
+ include Sidekiq::Worker
51
+ end
52
+
53
+ # Filter by worker class
54
+ Sidekiq.defer_jobs(SomeWorker) do
55
+ SomeWorker.perform_async(1)
56
+ # The SomeWorker job will not be enqueued yet
57
+
58
+ OtherWorker.perform_async(2)
59
+ # The OtherWorker job will be enqueued here since it doesn't match the filter
60
+ end
61
+ # The SomeWorker job will now be enqueued
62
+
63
+ # Filter by sidekiq_options
64
+ Sidekiq.defer_jobs(priority: "high") do
65
+ SomeWorker.perform_async(3)
66
+ # The SomeWorker job will not be enqueued yet
67
+
68
+ OtherWorker.perform_async(4)
69
+ # The OtherWorker job will be enqueued here since it doesn't match the filter
70
+ end
71
+ # The SomeWorker job will now be enqueued
72
+ ```
73
+
74
+ You can also pass `false` to `Sidekiq.defer_jobs` turn off deferral entirely within a block.
75
+
76
+ ```ruby
77
+ Sidekiq.defer_jobs(false) do
78
+ SomeWorker.perform_async(1)
79
+ # The SomeWorker job will be enqueued
80
+ end
81
+ ```
82
+
83
+ You can also manually control over when deferred jobs are enqueued or even remove previously deferred jobs.
84
+
85
+ ```ruby
86
+ Sidekiq.defer_jobs do
87
+ SomeWorker.perform_async(1)
88
+
89
+ # This will cancel SomeWorker.perform(1); it won't be enqueued
90
+ Sidekiq.abort_deferred_jobs!
91
+
92
+ SomeWorker.perform_async(2)
93
+ # SomeWorker.perform(2) is not yet equeued
94
+
95
+ Sidekiq.enqueue_deferred_jobs!
96
+ # SomeWorker.perform(2) will now be be equeued
97
+ end
98
+ ```
99
+
100
+ 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`.
101
+
102
+ 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.
103
+
104
+ ## Installation
105
+
106
+ Add this line to your application's Gemfile:
107
+
108
+ ```ruby
109
+ gem 'sidekiq-deferred_jobs'
110
+ ```
111
+
112
+ And then execute:
113
+ ```bash
114
+ $ bundle
115
+ ```
116
+
117
+ Or install it yourself as:
118
+ ```bash
119
+ $ gem install sidekiq-deferred_jobs
120
+ ```
121
+
122
+ ## Contributing
123
+
124
+ Open a pull request on GitHub.
125
+
126
+ Please use the [standardrb](https://github.com/testdouble/standard) syntax and lint your code with `standardrb --fix` before submitting.
127
+
128
+ ## License
129
+
130
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
@@ -0,0 +1,243 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq"
4
+
5
+ module Sidekiq
6
+ module DeferredJobs
7
+ class << self
8
+ # Defer enqueuing Sidekiq workers within the block until the end of the block.
9
+ # Any workers that normally would have been enqueued with a `perform_async` call
10
+ # will instead be queued up and run in an ensure clause at the end of the block.
11
+ # @param filter [Array<Module>, Array<Hash>] An array of either classes, modules, or hashes.
12
+ # If this is provided, only workers that match either a class or module or which have
13
+ # sidekiq_options that match a hash will be deferred. All other worker will be enqueued as normal.
14
+ # @return [void]
15
+ def defer(filter, &block)
16
+ jobs, filters = Thread.current[:sidekiq_deferred_jobs_jobs]
17
+ unless jobs
18
+ filters = []
19
+ jobs = Jobs.new
20
+ Thread.current[:sidekiq_deferred_jobs_jobs] = [jobs, filters]
21
+ end
22
+ filters.push(Filter.new(filter))
23
+ begin
24
+ yield
25
+ ensure
26
+ filters.pop
27
+ if filters.empty?
28
+ Thread.current[:sidekiq_deferred_jobs_jobs] = nil
29
+ jobs.enqueue!
30
+ end
31
+ end
32
+ end
33
+
34
+ # Disable deferred workers within the block. All workers will be enqueued normally
35
+ # within the block.
36
+ # @return [void]
37
+ def undeferred(&block)
38
+ save_val = Thread.current[:sidekiq_deferred_jobs_jobs]
39
+ begin
40
+ Thread.current[:sidekiq_deferred_jobs_jobs] = nil
41
+ yield
42
+ ensure
43
+ Thread.current[:sidekiq_deferred_jobs_jobs] = save_val
44
+ end
45
+ end
46
+
47
+ # Return true if the specified class with optional options should be deferred.
48
+ # @param klass [Class] A Sidekiq worker class
49
+ # @param opts [Hash, Nil] Optionsl options set at runtime for the worker.
50
+ # @return Boolean
51
+ def defer?(klass, opts = nil)
52
+ _jobs, filters = Thread.current[:sidekiq_deferred_jobs_jobs]
53
+ return false if filters.nil?
54
+ filters.any? { |filter| filter.match?(klass, opts) }
55
+ end
56
+
57
+ # Schedule a worker to be run at the end of the outermost defer block.
58
+ # @param klass [Class] Sidekiq worker class
59
+ # @param args [Array] Sidekiq job arguments
60
+ # @param opts [Hash, Nil] Optional sidekiq options specified for the job
61
+ # @return [void]
62
+ def defer_worker(klass, args, opts = nil)
63
+ jobs, _filters = Thread.current[:sidekiq_deferred_jobs_jobs]
64
+ if jobs
65
+ jobs.defer(klass, args, opts)
66
+ else
67
+ klass.perform_async(*args)
68
+ end
69
+ end
70
+ end
71
+
72
+ module DeferBlock
73
+ # Defer enqueuing Sidekiq workers within the block until the end of the block.
74
+ # Any workers that normally would have been enqueued with a `perform_async` call
75
+ # will instead be queued up and run in an ensure clause at the end of the block.
76
+ # @param *filter [Module>, Hash, FalseClass] Optional filter on which workers should be deferred.
77
+ # If a filter is specified, only matching workers will be deferred. To match the
78
+ # filter, the worker must either be the class specfied or include the module or
79
+ # have sidekiq_options that match the specified hash. If the filter is `false`
80
+ # then job deferral will be disabled entirely within the block.
81
+ # @return [void]
82
+ def defer_jobs(*filter, &block)
83
+ if filter.size == 1 && filter.first == false
84
+ Sidekiq::DeferredJobs.undeferred(&block)
85
+ else
86
+ Sidekiq::DeferredJobs.defer(filter, &block)
87
+ end
88
+ end
89
+
90
+ # Abort any already deferred Sidkiq workers in the current `defer_job` block.
91
+ # If a filter is specified, then only matching Sidekiq jobs will be aborted.
92
+ # @param *filter See #defer_job for filter specification.
93
+ # @return [Array<Sidekiq::DeferredJobs::Job>] the jobs that were aborted
94
+ def abort_deferred_jobs!(*filter)
95
+ jobs, _filters = Thread.current[:sidekiq_deferred_jobs_jobs]
96
+ if jobs
97
+ jobs.clear!(filter)
98
+ else
99
+ []
100
+ end
101
+ end
102
+
103
+ # Immediately enqueue any already deferred Sidkiq workers in the current `defer_job` block.
104
+ # If a filter is specified, then only matching Sidekiq jobs will be enqueued.
105
+ # @param *filter See #defer_job for filter specification.
106
+ # @return [void]
107
+ def enqueue_deferred_jobs!(*filter)
108
+ jobs, _filters = Thread.current[:sidekiq_deferred_jobs_jobs]
109
+ if jobs
110
+ Sidekiq::DeferredJobs.undeferred { jobs.enqueue!(filter) }
111
+ end
112
+ nil
113
+ end
114
+ end
115
+
116
+ # Override logic for Sidekiq::Worker.
117
+ module DeferredWorker
118
+ def perform_async(*args)
119
+ if Sidekiq::DeferredJobs.defer?(self)
120
+ Sidekiq::DeferredJobs.defer_worker(self, args)
121
+ else
122
+ super
123
+ end
124
+ end
125
+ end
126
+
127
+ # Override logic for Sidekiq::Worker::Setter.
128
+ module DeferredSetter
129
+ def perform_async(*args)
130
+ if Sidekiq::DeferredJobs.defer?(@klass, @opts)
131
+ Sidekiq::DeferredJobs.defer_worker(@klass, args, @opts)
132
+ else
133
+ super
134
+ end
135
+ end
136
+ end
137
+
138
+ # Logic for filtering jobs by worker class and/or sidekiq_options.
139
+ class Filter
140
+ def initialize(filters)
141
+ @filters = Array(filters).flatten
142
+ end
143
+
144
+ # @return [Boolean] true if the job matches the filters.
145
+ def match?(klass, opts = nil)
146
+ return true if @filters.empty?
147
+ @filters.any? do |filter|
148
+ if filter.is_a?(Module)
149
+ klass <= filter
150
+ elsif filter.is_a?(Hash)
151
+ worker_options = (opts ? klass.sidekiq_options.merge(opts.transform_keys(&:to_s)) : klass.sidekiq_options)
152
+ filter.all? { |key, value| worker_options[key.to_s] == value }
153
+ else
154
+ filter
155
+ end
156
+ end
157
+ end
158
+ end
159
+
160
+ # Data structure to hold job information.
161
+ Job = Struct.new(:klass, :args, :opts)
162
+
163
+ # Class for holding deferred jobs.
164
+ class Jobs
165
+ def initialize
166
+ @jobs = []
167
+ end
168
+
169
+ # Add a job to the deferred job list.
170
+ # @param klass [Class] Sidekiq worker class.
171
+ # @param args [Array] Sidekiq job arguments
172
+ # @param opts [Hash, Nil] optional runtime jobs options
173
+ def defer(klass, args, opts = nil)
174
+ @jobs << Job.new(klass, args&.dup, opts&.dup)
175
+ end
176
+
177
+ # Clear any deferred jobs that match the filter.
178
+ # @filter [Array<Module>, Array<Hash>] Filter for jobs to clear
179
+ def clear!(filters = nil)
180
+ filter = Filter.new(filters)
181
+ cleared_jobs = @jobs.select { |job| filter.match?(job.klass, job.opts) }
182
+ @jobs -= cleared_jobs
183
+ cleared_jobs
184
+ end
185
+
186
+ # Enqueue any deferred jobs that match the filter.
187
+ # @filter [Array<Module>, Array<Hash>] Filter for jobs to clear
188
+ def enqueue!(filters = nil)
189
+ filter = Filter.new(filters)
190
+ remaining_jobs = []
191
+ begin
192
+ duplicates = Set.new
193
+ @jobs.each do |job|
194
+ if filter.match?(job.klass, job.opts)
195
+ if unique_job?(job.klass, job.opts)
196
+ next if duplicates.include?([job.klass, job.args])
197
+ duplicates << [job.klass, job.args]
198
+ end
199
+ if job.opts
200
+ job.klass.set(job.opts).perform_async(*job.args)
201
+ else
202
+ job.klass.perform_async(*job.args)
203
+ end
204
+ else
205
+ remaining_jobs << job
206
+ end
207
+ end
208
+ ensure
209
+ @jobs = remaining_jobs
210
+ end
211
+ end
212
+
213
+ private
214
+
215
+ # @return [Boolean] true if the worker support a uniqueness constraint
216
+ def unique_job?(klass, opts)
217
+ enterprise_option = worker_options(klass, opts)["unique_for"] if defined?(Sidekiq::Enterprise)
218
+ unique_jobs_option = worker_options(klass, opts)["lock"] if defined?(SidekiqUniqueJobs)
219
+
220
+ if enterprise_option
221
+ true
222
+ elsif unique_jobs_option
223
+ unique_jobs_option.to_s != "while_executing"
224
+ else
225
+ false
226
+ end
227
+ end
228
+
229
+ # Merge runtime options with the worker class sidekiq_options.
230
+ def worker_options(klass, opts)
231
+ if opts
232
+ klass.sidekiq_options.merge(opts.transform_keys(&:to_s))
233
+ else
234
+ klass.sidekiq_options
235
+ end
236
+ end
237
+ end
238
+ end
239
+ end
240
+
241
+ Sidekiq.extend(Sidekiq::DeferredJobs::DeferBlock)
242
+ Sidekiq::Worker::ClassMethods.prepend(Sidekiq::DeferredJobs::DeferredWorker)
243
+ Sidekiq::Worker::Setter.prepend(Sidekiq::DeferredJobs::DeferredSetter)
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "sidekiq/deferred_jobs"
@@ -0,0 +1,34 @@
1
+ Gem::Specification.new do |spec|
2
+ spec.name = "sidekiq-deferred_jobs"
3
+ spec.version = File.read(File.expand_path("../VERSION", __FILE__)).strip
4
+ spec.authors = ["Brian Durand"]
5
+ spec.email = ["bbdurand@gmail.com"]
6
+
7
+ spec.summary = "Adds ability to defer the enqueuing of Sidekiq workers until the end of a block of code."
8
+ spec.homepage = "https://github.com/bdurand/sidekiq-deferred_jobs"
9
+ spec.license = "MIT"
10
+
11
+ # Specify which files should be added to the gem when it is released.
12
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
13
+ ignore_files = %w[
14
+ .
15
+ Appraisals
16
+ Gemfile
17
+ Gemfile.lock
18
+ Rakefile
19
+ bin/
20
+ gemfiles/
21
+ spec/
22
+ ]
23
+ spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do
24
+ `git ls-files -z`.split("\x0").reject { |f| ignore_files.any? { |path| f.start_with?(path) } }
25
+ end
26
+
27
+ spec.require_paths = ["lib"]
28
+
29
+ spec.add_dependency "sidekiq", ">= 5.0"
30
+
31
+ spec.add_development_dependency "bundler"
32
+
33
+ spec.required_ruby_version = ">= 2.5"
34
+ end
metadata ADDED
@@ -0,0 +1,79 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sidekiq-deferred_jobs
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Brian Durand
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-08-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: sidekiq
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '5.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '5.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description:
42
+ email:
43
+ - bbdurand@gmail.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - CHANGELOG.md
49
+ - MIT-LICENSE
50
+ - README.md
51
+ - VERSION
52
+ - lib/sidekiq-deferred_jobs.rb
53
+ - lib/sidekiq/deferred_jobs.rb
54
+ - sidekiq-deferred_jobs.gemspec
55
+ homepage: https://github.com/bdurand/sidekiq-deferred_jobs
56
+ licenses:
57
+ - MIT
58
+ metadata: {}
59
+ post_install_message:
60
+ rdoc_options: []
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '2.5'
68
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ requirements: []
74
+ rubygems_version: 3.0.3
75
+ signing_key:
76
+ specification_version: 4
77
+ summary: Adds ability to defer the enqueuing of Sidekiq workers until the end of a
78
+ block of code.
79
+ test_files: []