sidekiq-disposal 0.1.0

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 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: []