sidekiq-disposal 0.1.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: 8ecd41e2b17b1168b119bf60002c1680c960561f0f4c1d972648a08386ed1423
4
+ data.tar.gz: 14df2e7593177e8c2fb90d96d1acbe3ed57206987cb59763e06fdf9db4a7ba63
5
+ SHA512:
6
+ metadata.gz: 9e5e2a2084643fe72cafc7cbe7eb44dfacf500a63744d75585a6a38f63f8bf853090589183de163e487fbbeb975cd3e8e3ee8f11459c5b3e03a0ef890da2770e
7
+ data.tar.gz: 7bc4001093d88ef742f20102b5b11d98087726e9b076a7e733c8f695c200799a9c11282ef7c43d6b5d7de05fbd6a879ff2c83638e79ab86eaecc90b3e1aec24f
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format progress
2
+ --color
3
+ --require spec_helper
data/.standard.yml ADDED
@@ -0,0 +1,6 @@
1
+ # For available configuration options, see:
2
+ # https://github.com/standardrb/standard
3
+ ruby_version: 3.0
4
+ parallel: true
5
+ ignore:
6
+ - tmp/**/*
data/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ - A new thing!
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Hazel Bachrach, Steven Harman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,184 @@
1
+ # Sidekiq::Disposal
2
+
3
+ A [Sidekiq][sidekiq] extension mark Sidekiq Jobs to be disposed of based on the Job ID, Batch ID, or Job Class.
4
+ Disposal here means to either `:kill` the Job (send to the Dead queue) or `:discard` it (throw it away), at the time the job is picked up and processed by Sidekiq.
5
+ A disposed Job's `#perform` method will _not_ be called.
6
+
7
+ Disposing of queued Jobs is particularly useful as a mitigation technique during an incident.
8
+ For example, an issue with a 3rd party API that causes Jobs of a certain Class to take longer than expected/normal to run.
9
+ Or a code change/edge case that unexpectedly fans out more than expected, enqueuing a large volume of Jobs which then drown out other Jobs.
10
+ Or… any number of other ways that some Job, Batch, or Job Class has been enqueued, but you [don't want it to actually run][cancel-jobs].
11
+
12
+ `sidekiq-disposal` has your back!
13
+
14
+ ## Installation
15
+
16
+ Install the gem and add to the application's Gemfile by executing:
17
+
18
+ ```console
19
+ bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
20
+ ```
21
+
22
+ If bundler is not being used to manage dependencies, install the gem by executing:
23
+
24
+ ```console
25
+ gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ From a console (Rails console, or the like) you need a `Sidekiq::Disposal::Client` instance, which is used to `#mark` a Job, Batch, or Job Class to be disposed.
31
+
32
+ ```ruby
33
+ client = Sidekiq::Disposal::Client.new
34
+ ```
35
+
36
+ ### Marking to Kill
37
+
38
+ A Job marked to be killed means it will be moved to the Dead queue.
39
+
40
+ ```
41
+ # Mark a specific Job, by its ID to be killed
42
+ client.mark(:kill, :jid, some_job_id)
43
+
44
+ # Mark a Batch of Jobs to be killed, by Batch ID
45
+ client.mark(:kill, :bid, some_batch_id)
46
+
47
+ # Mark an entire Job Class to be killed
48
+ client.mark(:kill, :bid, "SomeJobClass")
49
+ ```
50
+
51
+ A Job, Batch, or Job Class can also be `#unmarked` for disposal via a corresponding API.
52
+
53
+ ```
54
+ # Un-mark a specific Job from being killed, by Job ID
55
+ client.unmark(:kill, :jid, some_job_id)
56
+
57
+ # Un-mark a Batch of Jobs from being killed, by Batch ID
58
+ client.unmark(:kill, :bid, some_batch_id)
59
+
60
+ # Un-mark an entire Job Class from being killed
61
+ client.unmark(:kill, :bid, "SomeJobClass")
62
+ ```
63
+
64
+ ### Marking to Drop
65
+
66
+ Similarly, a Job, Batch, or Job Class can be marked to be dropped.
67
+ Dropped jobs are discarded by Sidekiq - think of them as simply being deleted from the queue, without ever being run.
68
+
69
+ ```
70
+ # Mark a specific Job, by its ID to be dropped
71
+ client.mark(:drop, :jid, some_job_id)
72
+
73
+ # Mark a Batch of Jobs to be dropped, by Batch ID
74
+ client.mark(:drop, :bid, some_batch_id)
75
+
76
+ # Mark an entire Job Class to be dropped
77
+ client.mark(:drop, :bid, "SomeJobClass")
78
+ ```
79
+
80
+ And again, there is a corresponding API for un-marking a Job, Batch, or Job Class from being dropped.
81
+
82
+ ```
83
+ # Un-mark a specific Job from being dropped, by Job ID
84
+ client.unmark(:drop, :jid, some_job_id)
85
+
86
+ # Un-mark a Batch of Jobs from being dropped, by Batch ID
87
+ client.unmark(:drop, :bid, some_batch_id)
88
+
89
+ # Un-mark an entire Job Class from being dropped
90
+ client.unmark(:drop, :bid, "SomeJobClass")
91
+ ```
92
+
93
+ ### Un-marking All
94
+
95
+ Clearing all `:kill` or `:drop` marks can be done in one fell swoop as well.
96
+
97
+ ```ruby
98
+ client.unmark_all(:kill)
99
+
100
+ # or …
101
+
102
+ client.unmark_all(:drop)
103
+ ```
104
+
105
+ ## Configuration
106
+
107
+ With `sidekiq-dispose` installed, [register its Sidekiq server middleware][sidekiq-register-middleware].
108
+ Typically this is done via `config/initializers/sidekiq.rb` in a Rails app.
109
+
110
+ ```ruby
111
+ Sidekiq.configure_server do |config|
112
+ config.server_middleware do |chain|
113
+ chain.add Sidekiq::Disposal::ServerMiddleware
114
+ end
115
+ end
116
+ ```
117
+
118
+ This piece of middleware checks each job, after it's been dequeued, but before it's `#perform` has been called, to see if it should be disposed of.
119
+ If the job is marked for disposal (by Job ID, Batch ID, or Job Class) a corresponding error is raised by the middleware.
120
+
121
+ A Job marked `:kill` will raise a `Sidekiq::Disposal::JobKilled` error, while one marked `:drop` will raise `Sidekiq::Disposal::JobDropped`.
122
+ Out of the box these errors will cause [Sidekiq's error handling and retry mechanism][sidekiq-retries] to kick in, re-enqueueing the Job.
123
+ And round-and-round it will go until the default error/death handling kicks in.
124
+
125
+ To avoid this, you need to handle those specific `Sidekiq::Disposal` errors accordingly.
126
+
127
+ Adjust the base Sidekiq Job class, often called `ApplicationJob` or similar, to overwrite or tweak the `#sidekiq_retry_in` method:
128
+
129
+ ```ruby
130
+ sidekiq_retry_in do |_count, exception, jobhash|
131
+ case exception
132
+ when Infrastructure::Sidekiq::Disposal::JobKilled
133
+ # Optionally log/collect telemetry here too…
134
+ :kill
135
+ when Infrastructure::Sidekiq::Disposal::JobDropped
136
+ # Optionally log/collect telemetry here too…
137
+ :discard
138
+ end
139
+ end
140
+ ```
141
+
142
+ _NOTE_: If is not a base job, consider adding one, or you'll need to add this to every job you want to be disposable.
143
+
144
+ Returning `:kill` from this method will cause Sidekiq to immediately move the Job to the Dead Queue.
145
+ Similarly, returning `:discard` will cause Sidekiq to drop the job on the floor.
146
+ Either way, the Job's `#perform` is never called.
147
+
148
+ ### Non-Disposable Jobs
149
+
150
+ By default all Jobs are disposable, meaning they _can_ be marked to be `:kill`-ed or `:drop`-ed.
151
+ However, checking if a specific Job should be disposed of is not free; it requires round trip(s) to Redis.
152
+ Therefore, you might want to make some Jobs non-disposable to avoid these extra round trips.
153
+ Or because there are some Jobs that simply should never be disposed of for… _reasons_.
154
+
155
+ This is done via a Job's `sidekiq_options`.
156
+
157
+ ```ruby
158
+ sidekiq_options disposable: false
159
+ ```
160
+
161
+ With that in place, the server middleware will ignore the Job, and pass it down the middleware Chain.
162
+ No extra Redis calls, no funny business.
163
+
164
+ ## Development
165
+
166
+ After checking out the repo, run `bin/setup` to install dependencies.
167
+ Then, run `bin/rspec` to run the tests.
168
+ You can also run `bin/console` for an interactive prompt that will allow you to experiment.
169
+
170
+ To install this gem onto your local machine, run `bin/rake install`.
171
+ To release a new version, update the version number in `version.rb`, and then run `bin/rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
172
+
173
+ ## Contributing
174
+
175
+ Bug reports and pull requests are welcome on GitHub at https://github.com/hibachrach/sidekiq-disposal.
176
+
177
+ ## License
178
+
179
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
180
+
181
+ [sidekiq]: https://sidekiq.org "Simple, efficient background jobs for Ruby."
182
+ [sidekiq-register-middleware]: https://github.com/sidekiq/sidekiq/wiki/Middleware#registering-middleware "Registering Sidekiq Middleware"
183
+ [sidekiq-retries]: https://github.com/sidekiq/sidekiq/wiki/Error-Handling "Sidekiq Error Handling and Retries"
184
+ [cancel-jobs]: https://github.com/sidekiq/sidekiq/wiki/FAQ#how-do-i-cancel-a-sidekiq-job "How do I cancel a Sidekiq Job?"
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "standard/rake"
9
+
10
+ task default: %i[spec standard]
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq"
4
+
5
+ module Sidekiq
6
+ module Disposal
7
+ # A client for marking enqueued jobs for disposal. Disposal can be a job
8
+ # getting "killed" (sent straight to the dead queue/morgue) or "dropped"
9
+ # (hard deleted entirely).
10
+ #
11
+ # This task is accomplished with "markers": A job can be "marked" for a
12
+ # type of disposal. This means that a "marker" (a job id/jid, batch id/bid,
13
+ # or class name) can be formatted and then added to the relevant target
14
+ # set.
15
+ #
16
+ # When a worker picks up the job, the corresponding `ServerMiddleware` will
17
+ # then ensure that the job is not executed (see that class for more
18
+ # information).
19
+ class Client
20
+ REDIS_KILL_TARGET_SET = "sidekiq-disposal:kill_targets"
21
+ REDIS_DROP_TARGET_SET = "sidekiq-disposal:drop_targets"
22
+
23
+ ALLOWED_MARKER_TYPES = [
24
+ :jid,
25
+ :bid,
26
+ :class
27
+ ].freeze
28
+
29
+ def initialize(sidekiq_api = ::Sidekiq)
30
+ @sidekiq_api = sidekiq_api
31
+ end
32
+
33
+ # @param disposal_method [:kill, :drop] How to handle job
34
+ # @param marker_type [:jid, :bid, :class_name]
35
+ # @param marker [String]
36
+ def mark(disposal_method, marker_type, marker)
37
+ redis do |conn|
38
+ formatted_marker = formatted_marker(marker_type, marker)
39
+ disposal_target_set = disposal_target_set(disposal_method)
40
+ conn.sadd(disposal_target_set, formatted_marker)
41
+ end
42
+ end
43
+
44
+ # @param disposal_method [:kill, :drop] How to handle job
45
+ # @param marker_type [:jid, :bid, :class_name]
46
+ # @param marker [String]
47
+ def unmark(disposal_method, marker_type, marker)
48
+ redis do |conn|
49
+ formatted_marker = formatted_marker(marker_type, marker)
50
+ disposal_target_set = disposal_target_set(disposal_method)
51
+ conn.srem(disposal_target_set, formatted_marker)
52
+ end
53
+ end
54
+
55
+ def unmark_all(disposal_method)
56
+ redis do |conn|
57
+ conn.del(disposal_target_set(disposal_method))
58
+ end
59
+ end
60
+
61
+ def markers(disposal_method)
62
+ redis do |conn|
63
+ conn.smembers(disposal_target_set(disposal_method))
64
+ end
65
+ end
66
+
67
+ def kill_target?(job)
68
+ job_in_target_set?(job, disposal_target_set(:kill))
69
+ end
70
+
71
+ def drop_target?(job)
72
+ job_in_target_set?(job, disposal_target_set(:drop))
73
+ end
74
+
75
+ private
76
+
77
+ def redis(&blk)
78
+ sidekiq_api.redis(&blk)
79
+ end
80
+
81
+ def disposal_target_set(disposal_method)
82
+ case disposal_method
83
+ when :kill
84
+ REDIS_KILL_TARGET_SET
85
+ when :drop
86
+ REDIS_DROP_TARGET_SET
87
+ else
88
+ raise ArgumentError, "disposal_method must be either :kill or :drop, instead got: #{disposal_method}"
89
+ end
90
+ end
91
+
92
+ def job_in_target_set?(job, target_set)
93
+ redis do |conn|
94
+ # `SMISEMBERS setname element1 [element2 ...]` asks whether each
95
+ # element given is in `setname`; redis-client (the low-level redis
96
+ # api used by Sidekiq) returns an array of integer answers for
97
+ # each element: 1 if it's a member, and 0 otherwise.
98
+ conn.smismember(target_set, formatted_markers(job)).any? do |match|
99
+ match == 1
100
+ end
101
+ end
102
+ end
103
+
104
+ # @return [Array] A list of identifying formatted_markers/features which
105
+ # indicates a job is targeted for disposal.
106
+ def formatted_markers(job)
107
+ ALLOWED_MARKER_TYPES.map do |marker_type|
108
+ formatted_marker_for_job(marker_type, job)
109
+ end.compact
110
+ end
111
+
112
+ # @returns the formatted marker that would be in Redis if this job has
113
+ # been targeted
114
+ def formatted_marker_for_job(marker_type, job)
115
+ formatted_marker(marker_type, job[marker_type.to_s])
116
+ end
117
+
118
+ # @returns the marker as it is stored in Redis
119
+ def formatted_marker(marker_type, marker)
120
+ return nil if marker.nil?
121
+ raise ArgumentError unless ALLOWED_MARKER_TYPES.include?(marker_type.to_sym)
122
+ "#{marker_type}:#{marker}"
123
+ end
124
+
125
+ attr_reader :sidekiq_api
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq"
4
+ require_relative "../disposal"
5
+
6
+ module Sidekiq
7
+ module Disposal
8
+ class ServerMiddleware
9
+ include ::Sidekiq::ServerMiddleware
10
+
11
+ def initialize(client = Client.new)
12
+ @client = client
13
+ end
14
+
15
+ def call(job_instance, job, _queue)
16
+ if job_instance && !job_instance.class.get_sidekiq_options.fetch("disposable", true)
17
+ yield
18
+ elsif client.kill_target?(job)
19
+ raise JobKilled
20
+ elsif client.drop_target?(job)
21
+ raise JobDropped
22
+ else
23
+ yield
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :client
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module Disposal
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "disposal/version"
4
+ require_relative "disposal/client"
5
+ require_relative "disposal/server_middleware"
6
+
7
+ module Sidekiq
8
+ # Namespace for everything related to job disposal: the process of putting
9
+ # jobs' markers (i.e. identifying features) on a list so they can be "killed"
10
+ # (sent immediately to dead set/morgue) or "dropped" (completely discarded
11
+ # from Sidekiq) when picked up from the queue.
12
+ module Disposal
13
+ Error = Class.new(StandardError)
14
+ JobKilled = Class.new(Error)
15
+ JobDropped = Class.new(Error)
16
+ end
17
+ end
@@ -0,0 +1,6 @@
1
+ module Sidekiq
2
+ module Disposal
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end
metadata ADDED
@@ -0,0 +1,77 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sidekiq-disposal
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Hazel Bachrach
8
+ - Steven Harman
9
+ autorequire:
10
+ bindir: exe
11
+ cert_chain: []
12
+ date: 2024-12-13 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: sidekiq
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '7.0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '7.0'
28
+ description: |
29
+ A mechanism to mark Sidekiq Jobs to be disposed of by Job ID, Batch ID, or Job Class.
30
+ Disposal here means to either `:kill` the Job (send to the Dead queue) or `:discard` it (throw it away), at the time the job is picked up and processed by Sidekiq.
31
+ email:
32
+ - dacheatbot@gmail.com
33
+ - steven@harmanly.com
34
+ executables: []
35
+ extensions: []
36
+ extra_rdoc_files: []
37
+ files:
38
+ - ".rspec"
39
+ - ".standard.yml"
40
+ - CHANGELOG.md
41
+ - LICENSE.txt
42
+ - README.md
43
+ - Rakefile
44
+ - lib/sidekiq/disposal.rb
45
+ - lib/sidekiq/disposal/client.rb
46
+ - lib/sidekiq/disposal/server_middleware.rb
47
+ - lib/sidekiq/disposal/version.rb
48
+ - sig/sidekiq/disposal.rbs
49
+ homepage: https://github.com/hibachrach/sidekiq-disposal
50
+ licenses:
51
+ - MIT
52
+ metadata:
53
+ changelog_uri: https://github.com/hibachrach/sidekiq-disposal/blob/main/CHANGELOG.md
54
+ documentation_uri: https://github.com/hibachrach/sidekiq-disposal
55
+ homepage_uri: https://github.com/hibachrach/sidekiq-disposal
56
+ source_code_uri: https://github.com/hibachrach/sidekiq-disposal
57
+ post_install_message:
58
+ rdoc_options: []
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: 3.0.0
66
+ required_rubygems_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ requirements: []
72
+ rubygems_version: 3.5.23
73
+ signing_key:
74
+ specification_version: 4
75
+ summary: A mechanism to dispose of (cancel) queued jobs by Job ID, Batch ID, or Job
76
+ Class.
77
+ test_files: []