sidekiq_utils 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +16 -0
- data/Gemfile.lock +96 -0
- data/LICENSE +21 -0
- data/README.md +185 -0
- data/Rakefile +50 -0
- data/VERSION +1 -0
- data/lib/sidekiq_utils/additional_serialization.rb +56 -0
- data/lib/sidekiq_utils/deprioritize.rb +19 -0
- data/lib/sidekiq_utils/enqueued_jobs_helper.rb +45 -0
- data/lib/sidekiq_utils/find_optional.rb +24 -0
- data/lib/sidekiq_utils/job_counter.rb +161 -0
- data/lib/sidekiq_utils/latency_alert.rb +97 -0
- data/lib/sidekiq_utils/middleware/client/additional_serialization.rb +20 -0
- data/lib/sidekiq_utils/middleware/client/deprioritize.rb +14 -0
- data/lib/sidekiq_utils/middleware/client/job_counter.rb +17 -0
- data/lib/sidekiq_utils/middleware/server/additional_serialization.rb +20 -0
- data/lib/sidekiq_utils/middleware/server/find_optional.rb +20 -0
- data/lib/sidekiq_utils/middleware/server/job_counter.rb +15 -0
- data/lib/sidekiq_utils/middleware/server/memory_monitor.rb +46 -0
- data/lib/sidekiq_utils/middleware/server/throughput_monitor.rb +25 -0
- data/lib/sidekiq_utils/redis_monitor_storage.rb +81 -0
- data/lib/sidekiq_utils/views/job_counts.erb +34 -0
- data/lib/sidekiq_utils/views/memory.erb +33 -0
- data/lib/sidekiq_utils/views/throughput.erb +27 -0
- data/lib/sidekiq_utils/web_extensions/job_counter.rb +35 -0
- data/lib/sidekiq_utils/web_extensions/memory_monitor.rb +26 -0
- data/lib/sidekiq_utils/web_extensions/throughput_monitor.rb +32 -0
- data/lib/sidekiq_utils.rb +21 -0
- data/sidekiq_utils.gemspec +84 -0
- metadata +158 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: b3f6eac2a6e7d56402251588cfe585dd2ab0b489
|
4
|
+
data.tar.gz: 2b5c6aab41b220464b6ce061f66128be1a9ac006
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 33f07d894defb62273a7722d19c4ee604cef8a4c9f9f17291a82e89f7614632f108fc58e8fdb8420a5407d6a0f5c07d18125e69b4a92f762614d24b9e6be52e7
|
7
|
+
data.tar.gz: 124dde56aa5b9bd3525974e42177b6a1ccd4ec4340d98fdee0fba8a4d0ae106825a560a5bdd0a111842297fbfdc1870ff6f5ad1dad5f439d631bc07ec68a9987
|
data/Gemfile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
source "https://rubygems.org"
|
2
|
+
#
|
3
|
+
# Add dependencies required to use your gem here.
|
4
|
+
gem "sidekiq", ">= 4.0.0"
|
5
|
+
# Example:
|
6
|
+
# gem "activesupport", ">= 2.3.5"
|
7
|
+
|
8
|
+
# Add dependencies to develop your gem here.
|
9
|
+
# Include everything needed to run rake, tests, features, etc.
|
10
|
+
group :development do
|
11
|
+
gem "shoulda", ">= 0"
|
12
|
+
gem "rdoc", "~> 3.12"
|
13
|
+
gem "bundler", "~> 1.0"
|
14
|
+
gem "juwelier", "~> 2.1.0"
|
15
|
+
gem "simplecov", ">= 0"
|
16
|
+
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
GEM
|
2
|
+
remote: https://rubygems.org/
|
3
|
+
specs:
|
4
|
+
activesupport (5.1.4)
|
5
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
6
|
+
i18n (~> 0.7)
|
7
|
+
minitest (~> 5.1)
|
8
|
+
tzinfo (~> 1.1)
|
9
|
+
addressable (2.5.2)
|
10
|
+
public_suffix (>= 2.0.2, < 4.0)
|
11
|
+
builder (3.2.3)
|
12
|
+
concurrent-ruby (1.0.5)
|
13
|
+
connection_pool (2.2.1)
|
14
|
+
descendants_tracker (0.0.4)
|
15
|
+
thread_safe (~> 0.3, >= 0.3.1)
|
16
|
+
docile (1.1.5)
|
17
|
+
faraday (0.12.2)
|
18
|
+
multipart-post (>= 1.2, < 3)
|
19
|
+
git (1.3.0)
|
20
|
+
github_api (0.18.2)
|
21
|
+
addressable (~> 2.4)
|
22
|
+
descendants_tracker (~> 0.0.4)
|
23
|
+
faraday (~> 0.8)
|
24
|
+
hashie (~> 3.5, >= 3.5.2)
|
25
|
+
oauth2 (~> 1.0)
|
26
|
+
hashie (3.5.6)
|
27
|
+
highline (1.7.10)
|
28
|
+
i18n (0.9.1)
|
29
|
+
concurrent-ruby (~> 1.0)
|
30
|
+
json (1.8.6)
|
31
|
+
juwelier (2.1.3)
|
32
|
+
builder
|
33
|
+
bundler (>= 1.13)
|
34
|
+
git (>= 1.2.5)
|
35
|
+
github_api
|
36
|
+
highline (>= 1.6.15)
|
37
|
+
nokogiri (>= 1.5.10)
|
38
|
+
rake
|
39
|
+
rdoc
|
40
|
+
semver
|
41
|
+
jwt (1.5.6)
|
42
|
+
mini_portile2 (2.3.0)
|
43
|
+
minitest (5.10.3)
|
44
|
+
multi_json (1.12.2)
|
45
|
+
multi_xml (0.6.0)
|
46
|
+
multipart-post (2.0.0)
|
47
|
+
nokogiri (1.8.1)
|
48
|
+
mini_portile2 (~> 2.3.0)
|
49
|
+
oauth2 (1.4.0)
|
50
|
+
faraday (>= 0.8, < 0.13)
|
51
|
+
jwt (~> 1.0)
|
52
|
+
multi_json (~> 1.3)
|
53
|
+
multi_xml (~> 0.5)
|
54
|
+
rack (>= 1.2, < 3)
|
55
|
+
public_suffix (3.0.1)
|
56
|
+
rack (2.0.3)
|
57
|
+
rack-protection (2.0.0)
|
58
|
+
rack
|
59
|
+
rake (12.3.0)
|
60
|
+
rdoc (3.12.2)
|
61
|
+
json (~> 1.4)
|
62
|
+
redis (4.0.1)
|
63
|
+
semver (1.0.1)
|
64
|
+
shoulda (3.5.0)
|
65
|
+
shoulda-context (~> 1.0, >= 1.0.1)
|
66
|
+
shoulda-matchers (>= 1.4.1, < 3.0)
|
67
|
+
shoulda-context (1.2.2)
|
68
|
+
shoulda-matchers (2.8.0)
|
69
|
+
activesupport (>= 3.0.0)
|
70
|
+
sidekiq (5.0.5)
|
71
|
+
concurrent-ruby (~> 1.0)
|
72
|
+
connection_pool (~> 2.2, >= 2.2.0)
|
73
|
+
rack-protection (>= 1.5.0)
|
74
|
+
redis (>= 3.3.4, < 5)
|
75
|
+
simplecov (0.15.1)
|
76
|
+
docile (~> 1.1.0)
|
77
|
+
json (>= 1.8, < 3)
|
78
|
+
simplecov-html (~> 0.10.0)
|
79
|
+
simplecov-html (0.10.2)
|
80
|
+
thread_safe (0.3.6)
|
81
|
+
tzinfo (1.2.4)
|
82
|
+
thread_safe (~> 0.1)
|
83
|
+
|
84
|
+
PLATFORMS
|
85
|
+
ruby
|
86
|
+
|
87
|
+
DEPENDENCIES
|
88
|
+
bundler (~> 1.0)
|
89
|
+
juwelier (~> 2.1.0)
|
90
|
+
rdoc (~> 3.12)
|
91
|
+
shoulda
|
92
|
+
sidekiq (>= 4.0.0)
|
93
|
+
simplecov
|
94
|
+
|
95
|
+
BUNDLED WITH
|
96
|
+
1.16.0
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2017 VentureHacks
|
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 all
|
13
|
+
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 THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,185 @@
|
|
1
|
+
# sidekiq_utils
|
2
|
+
Sidekiq powers our background processing needs at AngelList. As we introduced Sidekiq in a legacy codebase and it started to handle significant job throughput, we developed a number of utilities to make working with Sidekiq easier that we'd love to share.
|
3
|
+
|
4
|
+
## Additional Serialization
|
5
|
+
|
6
|
+
Adds support for automatically serializing and deserializing additional argument types for your workers:
|
7
|
+
* `Class`
|
8
|
+
* `Symbol` (both by themselves and as hash keys)
|
9
|
+
* `ActiveSupport::HashWithIndifferentAccess`
|
10
|
+
|
11
|
+
To use this utility, you need to include the client and server middlewares. Be sure to include the client middleware on the server as well.
|
12
|
+
|
13
|
+
### Configuration
|
14
|
+
```
|
15
|
+
Sidekiq.configure_server do |config|
|
16
|
+
config.server_middleware do |chain|
|
17
|
+
chain.add ::SidekiqUtils::Middleware::Server::AdditionalSerialization
|
18
|
+
end
|
19
|
+
config.client_middleware do |chain|
|
20
|
+
chain.add ::SidekiqUtils::Middleware::Client::AdditionalSerialization
|
21
|
+
end
|
22
|
+
end
|
23
|
+
Sidekiq.configure_client do |config|
|
24
|
+
config.client_middleware do |chain|
|
25
|
+
chain.add ::SidekiqUtils::Middleware::Client::AdditionalSerialization
|
26
|
+
end
|
27
|
+
end
|
28
|
+
```
|
29
|
+
|
30
|
+
### A note of caution for users of the sidekiq-unique-jobs gem
|
31
|
+
|
32
|
+
sidekiq-unique-jobs will fail to properly determine job uniqueness if you don't hook the middlewares in the correct order. If you use both sidekiq-unique-jobs and the additional serialization utility, be sure to hook your middlewares in the following way:
|
33
|
+
```
|
34
|
+
Sidekiq.configure_server do |config|
|
35
|
+
# unwrap arguments first
|
36
|
+
config.server_middleware do |chain|
|
37
|
+
chain.add ::SidekiqUtils::Middleware::Server::AdditionalSerialization
|
38
|
+
end
|
39
|
+
# then determine unique jobs
|
40
|
+
SidekiqUniqueJobs.configure_server_middleware
|
41
|
+
|
42
|
+
# first determine uniqueness
|
43
|
+
SidekiqUniqueJobs.configure_client_middleware
|
44
|
+
# then wrap arguments
|
45
|
+
config.client_middleware do |chain|
|
46
|
+
chain.add ::SidekiqUtils::Middleware::Client::AdditionalSerialization
|
47
|
+
end
|
48
|
+
end
|
49
|
+
Sidekiq.configure_client do |config|
|
50
|
+
# first determine uniqueness
|
51
|
+
SidekiqUniqueJobs.configure_client_middleware
|
52
|
+
# then wrap arguments
|
53
|
+
config.client_middleware do |chain|
|
54
|
+
chain.add ::SidekiqUtils::Middleware::Client::AdditionalSerialization
|
55
|
+
end
|
56
|
+
end
|
57
|
+
```
|
58
|
+
|
59
|
+
## Deprioritize
|
60
|
+
|
61
|
+
Adds an easy ability to divert jobs added within a block to a lower-priority queue. This is useful, for example, in cron jobs. All jobs added within the block will be added to the `low` queue instead of their default queue.
|
62
|
+
|
63
|
+
### Configuration
|
64
|
+
|
65
|
+
```
|
66
|
+
Sidekiq.configure_server do |config|
|
67
|
+
config.client_middleware do |chain|
|
68
|
+
chain.add SidekiqUtils::Middleware::Client::Deprioritize
|
69
|
+
end
|
70
|
+
end
|
71
|
+
Sidekiq.configure_client do |config|
|
72
|
+
config.client_middleware do |chain|
|
73
|
+
client_middlewares.each do |middleware|
|
74
|
+
chain.add SidekiqUtils::Middleware::Client::Deprioritize
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
```
|
79
|
+
|
80
|
+
### Usage
|
81
|
+
|
82
|
+
```
|
83
|
+
SidekiqUtils::Deprioritize.workers(SolrIndexWorker) do
|
84
|
+
10_000.times do
|
85
|
+
# these will all go to the `low` queue
|
86
|
+
SolrIndexWorker.perform_async(User, rand(10_000))
|
87
|
+
end
|
88
|
+
end
|
89
|
+
```
|
90
|
+
|
91
|
+
## Enqueued jobs helper
|
92
|
+
|
93
|
+
A simple tool to inspect and manipulate Sidekiq queues from the console:
|
94
|
+
|
95
|
+
```
|
96
|
+
> SidekiqUtils::EnqueuedJobsHelper.counts
|
97
|
+
=> {"default"=>{}, "high"=>{"AlgoliaIndexWorker[JobProfile]"=>1}, "low"=>{"AlgoliaIndexWorker[JobProfile]"=>1}}
|
98
|
+
|
99
|
+
> SidekiqUtils::EnqueuedJobsHelper.delete(queue: 'low', job_class: 'AlgoliaIndexWorker', first_argument: JobProfile)
|
100
|
+
# first_argument is optional
|
101
|
+
```
|
102
|
+
|
103
|
+
## Find optional
|
104
|
+
|
105
|
+
Lots of jobs become moot when a record cannot be found; however, because Sidekiq is very fast and it's easy to forget to enqueue jobs in `after_commit` hooks, sometimes the record isn't found simply because the transaction in which it was inserted has not been committed yet. This will retry a `find_optional` call exactly once after 30 seconds if the record cannot be found.
|
106
|
+
|
107
|
+
### Configuration
|
108
|
+
|
109
|
+
```
|
110
|
+
Sidekiq.configure_server do |config|
|
111
|
+
config.server_middleware do |chain|
|
112
|
+
chain.add SidekiqUtils::Middleware::Server::FindOptional
|
113
|
+
end
|
114
|
+
end
|
115
|
+
```
|
116
|
+
|
117
|
+
### Usage
|
118
|
+
|
119
|
+
```
|
120
|
+
class SolrIndexWorker
|
121
|
+
include Sidekiq::Worker
|
122
|
+
|
123
|
+
def perform(user_id)
|
124
|
+
user = find_optional(User, user_id)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
```
|
128
|
+
|
129
|
+
## Job counter
|
130
|
+
|
131
|
+
This will keep a running count of jobs per queue and worker class so that you can inspect what is clogging up high-volume queues without having to iterate over the entire queue.
|
132
|
+
|
133
|
+
### Configuration
|
134
|
+
|
135
|
+
```
|
136
|
+
SidekiqUtils::JobCounter.hook_sidekiq!
|
137
|
+
Sidekiq::Web.register SidekiqUtils::WebExtensions::JobCounter
|
138
|
+
Sidekiq::Web.tabs["Job counts"] = "job_counts"
|
139
|
+
```
|
140
|
+
|
141
|
+
### Usage
|
142
|
+
|
143
|
+
This will add a "Job counts" tab to your Sidekiq admin which will display the current job count.
|
144
|
+
|
145
|
+
Please note that the accuracy of these numbers is not guaranteed as it is non-trivial to keep a running count. However, this is a useful tool when you are inspecting a very long queue.
|
146
|
+
|
147
|
+
## Memory monitor
|
148
|
+
|
149
|
+
This automatically checks memory usage before and after a worker is run and keeps track of which jobs consistently leak memory. Please note that this is very approximate. It also requires you running one worker process single-threaded with `-c 1`. It slows down jobs processed by that worker considerably.
|
150
|
+
|
151
|
+
### Configuration
|
152
|
+
|
153
|
+
```
|
154
|
+
Sidekiq.configure_server do |config|
|
155
|
+
config.server_middleware do |chain|
|
156
|
+
chain.add SidekiqUtils::Middleware::Server::MemoryMonitor
|
157
|
+
end
|
158
|
+
end
|
159
|
+
Sidekiq::Web.register SidekiqUtils::WebExtensions::MemoryMonitor
|
160
|
+
Sidekiq::Web.tabs["Memory"] = "memory"
|
161
|
+
```
|
162
|
+
|
163
|
+
### Usage
|
164
|
+
|
165
|
+
This will add a "Memory" tab to your Sidekiq admin which will display memory usage information.
|
166
|
+
|
167
|
+
## Throughput monitor
|
168
|
+
|
169
|
+
This will keep track of how many jobs of which worker class have run in the past week, as well as when it was last run.
|
170
|
+
|
171
|
+
### Configuration
|
172
|
+
|
173
|
+
```
|
174
|
+
Sidekiq.configure_server do |config|
|
175
|
+
config.server_middleware do |chain|
|
176
|
+
chain.add SidekiqUtils::Middleware::Server::ThroughputMonitor
|
177
|
+
end
|
178
|
+
end
|
179
|
+
Sidekiq::Web.register SidekiqUtils::WebExtensions::ThroughputMonitor
|
180
|
+
Sidekiq::Web.tabs["Throughput"] = "throughput"
|
181
|
+
```
|
182
|
+
|
183
|
+
### Usage
|
184
|
+
|
185
|
+
This will add a "Throughput" tab to your Sidekiq admin which will display job throughput information.
|
data/Rakefile
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
begin
|
6
|
+
Bundler.setup(:default, :development)
|
7
|
+
rescue Bundler::BundlerError => e
|
8
|
+
$stderr.puts e.message
|
9
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
10
|
+
exit e.status_code
|
11
|
+
end
|
12
|
+
require 'rake'
|
13
|
+
require 'juwelier'
|
14
|
+
Juwelier::Tasks.new do |gem|
|
15
|
+
# gem is a Gem::Specification... see http://guides.rubygems.org/specification-reference/ for more options
|
16
|
+
gem.name = "sidekiq_utils"
|
17
|
+
gem.homepage = "http://github.com/venturehacks/sidekiq_angels"
|
18
|
+
gem.license = "MIT"
|
19
|
+
gem.summary = %Q{Tools that make working with a major Sidekiq installation more fun.}
|
20
|
+
gem.description = %Q{Tools that make working with a major Sidekiq installation more fun.}
|
21
|
+
gem.email = "magnus@angel.co"
|
22
|
+
gem.authors = ["Magnus von Koeller"]
|
23
|
+
|
24
|
+
# dependencies defined in Gemfile
|
25
|
+
end
|
26
|
+
Juwelier::RubygemsDotOrgTasks.new
|
27
|
+
require 'rake/testtask'
|
28
|
+
Rake::TestTask.new(:test) do |test|
|
29
|
+
test.libs << 'lib' << 'test'
|
30
|
+
test.pattern = 'test/**/test_*.rb'
|
31
|
+
test.verbose = true
|
32
|
+
end
|
33
|
+
|
34
|
+
desc "Code coverage detail"
|
35
|
+
task :simplecov do
|
36
|
+
ENV['COVERAGE'] = "true"
|
37
|
+
Rake::Task['test'].execute
|
38
|
+
end
|
39
|
+
|
40
|
+
task :default => :test
|
41
|
+
|
42
|
+
require 'rdoc/task'
|
43
|
+
Rake::RDocTask.new do |rdoc|
|
44
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
45
|
+
|
46
|
+
rdoc.rdoc_dir = 'rdoc'
|
47
|
+
rdoc.title = "sidekiq_angels #{version}"
|
48
|
+
rdoc.rdoc_files.include('README*')
|
49
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
50
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.0.0
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module SidekiqUtils
|
2
|
+
module AdditionalSerialization
|
3
|
+
def self.wrap_argument(arg)
|
4
|
+
if arg.is_a?(Array)
|
5
|
+
arg.map {|a| wrap_argument(a) }
|
6
|
+
elsif arg.is_a?(Hash)
|
7
|
+
wrapped = arg.each_with_object({}) do |(key, value), hash|
|
8
|
+
hash[key.to_s] = wrap_argument(value)
|
9
|
+
end
|
10
|
+
symbol_keys = arg.each_key.grep(Symbol).map(&:to_s)
|
11
|
+
wrapped['_al_aj_symbol_keys'] = symbol_keys if symbol_keys.present?
|
12
|
+
if arg.is_a?(ActiveSupport::HashWithIndifferentAccess)
|
13
|
+
wrapped['_al_aj_indifferent_access'] = true
|
14
|
+
end
|
15
|
+
wrapped
|
16
|
+
elsif arg.is_a?(Symbol)
|
17
|
+
{ '_al_aj_wrapped' => 'symbol', 'value' => arg.to_s }
|
18
|
+
elsif arg.is_a?(Class)
|
19
|
+
{ '_al_aj_wrapped' => 'class', 'value' => arg.name }
|
20
|
+
else
|
21
|
+
arg
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.unwrap_argument(arg)
|
26
|
+
if arg.is_a?(Hash) && arg['_al_aj_wrapped'].present?
|
27
|
+
case arg['_al_aj_wrapped']
|
28
|
+
when 'symbol'
|
29
|
+
arg['value'].to_sym
|
30
|
+
when 'class'
|
31
|
+
arg['value'].constantize
|
32
|
+
else
|
33
|
+
fail("Unknown wrapped value: #{arg['_al_aj_wrapped']}")
|
34
|
+
end
|
35
|
+
elsif arg.is_a?(Hash)
|
36
|
+
# make sure that we don't accidentally mess with the argument here,
|
37
|
+
# rather the caller should be responsible for actually replacing values
|
38
|
+
arg = arg.deep_dup
|
39
|
+
|
40
|
+
symbol_keys = arg.delete('_al_aj_symbol_keys') || []
|
41
|
+
unwrapped = arg.each_with_object({}) do |(key, value), hash|
|
42
|
+
key = key.to_sym if symbol_keys.include?(key)
|
43
|
+
hash[key] = unwrap_argument(value)
|
44
|
+
end
|
45
|
+
if unwrapped.delete('_al_aj_indifferent_access')
|
46
|
+
unwrapped = unwrapped.with_indifferent_access
|
47
|
+
end
|
48
|
+
unwrapped
|
49
|
+
elsif arg.is_a?(Array)
|
50
|
+
arg.map {|a| unwrap_argument(a) }
|
51
|
+
else
|
52
|
+
arg
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module SidekiqUtils
|
2
|
+
module Deprioritize
|
3
|
+
def self.workers(*workers)
|
4
|
+
workers = workers.map do |worker|
|
5
|
+
if worker.is_a?(Class)
|
6
|
+
worker.name
|
7
|
+
else
|
8
|
+
worker
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
old_deprioritized = Thread.current[:deprioritize_worker_classes]
|
13
|
+
Thread.current[:deprioritize_worker_classes] ||= []
|
14
|
+
Thread.current[:deprioritize_worker_classes] |= workers
|
15
|
+
yield
|
16
|
+
Thread.current[:deprioritize_worker_classes] = old_deprioritized
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module SidekiqUtils
|
2
|
+
module EnqueuedJobsHelper
|
3
|
+
class << self
|
4
|
+
def counts
|
5
|
+
counts = {}
|
6
|
+
Sidekiq::Queue.all.each do |queue|
|
7
|
+
queue_job_counts = counts[queue.name] ||= {}
|
8
|
+
queue.each do |job|
|
9
|
+
job_key = SidekiqUtils::RedisMonitorStorage.job_prefix(
|
10
|
+
job, unwrap_arguments: true)
|
11
|
+
queue_job_counts[job_key] ||= 0
|
12
|
+
queue_job_counts[job_key] += 1
|
13
|
+
end
|
14
|
+
end
|
15
|
+
counts
|
16
|
+
end
|
17
|
+
|
18
|
+
def delete(queue:, job_class:, first_argument: nil)
|
19
|
+
if job_class.is_a?(Class)
|
20
|
+
job_class = job_class.name
|
21
|
+
else
|
22
|
+
job_class = job_class.to_s
|
23
|
+
end
|
24
|
+
|
25
|
+
deleted = 0
|
26
|
+
Sidekiq::Queue.all.each do |iter_queue|
|
27
|
+
next if queue && queue.to_s != iter_queue.name
|
28
|
+
|
29
|
+
iter_queue.each do |job|
|
30
|
+
next if job['class'] != job_class
|
31
|
+
if first_argument && SidekiqUtils::AdditionalSerialization.
|
32
|
+
unwrap_argument(job['args'].first) != first_argument
|
33
|
+
next
|
34
|
+
end
|
35
|
+
|
36
|
+
job.delete
|
37
|
+
deleted += 1
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
deleted
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module SidekiqUtils
|
2
|
+
module FindOptional
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
class NotFoundError < StandardError; end
|
6
|
+
|
7
|
+
# try finding a record, but eventually give up retrying if we still cannot
|
8
|
+
# find it. use this if you are trying to load a record from a job, but
|
9
|
+
# don't want the failed job to end up in the RetrySet if it keeps failing.
|
10
|
+
def find_optional(entity, id, scope: nil)
|
11
|
+
entity = entity.public_send(scope) if scope
|
12
|
+
if id.is_a?(Enumerable)
|
13
|
+
instance = entity.where(id: id)
|
14
|
+
else
|
15
|
+
instance = entity.find_by(id: id)
|
16
|
+
end
|
17
|
+
if !instance.present?
|
18
|
+
fail(NotFoundError)
|
19
|
+
else
|
20
|
+
instance
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
module SidekiqUtils
|
2
|
+
module JobCounter
|
3
|
+
REDIS_KEY = 'sidekiq_utils_job_counter'
|
4
|
+
LOCK = Mutex.new
|
5
|
+
SYNC_COUNTS_EVERY = 1.second
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def increment(job)
|
9
|
+
change_count(job, 1)
|
10
|
+
end
|
11
|
+
|
12
|
+
def decrement(job)
|
13
|
+
change_count(job, -1)
|
14
|
+
end
|
15
|
+
|
16
|
+
def counts
|
17
|
+
Sidekiq.redis do |redis|
|
18
|
+
redis.hgetall(REDIS_KEY).each_with_object({}) do |(key, count), ret_hash|
|
19
|
+
key_hash = JSON.parse(key, symbolize_names: true)
|
20
|
+
|
21
|
+
count = count.to_i
|
22
|
+
if count != 0
|
23
|
+
ret_hash[key_hash[:queue]] ||= {}
|
24
|
+
ret_hash[key_hash[:queue]][key_hash[:job]] = count
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def reset!
|
31
|
+
Sidekiq.redis do |redis|
|
32
|
+
redis.del(REDIS_KEY)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def hook_sidekiq!
|
37
|
+
Sidekiq::SortedEntry.prepend(SortedEntry::JobCounterExtension)
|
38
|
+
Sidekiq::ScheduledSet.prepend(ScheduledSet::JobCounterExtension)
|
39
|
+
Sidekiq::Job.prepend(Job::JobCounterExtension)
|
40
|
+
Sidekiq::Queue.prepend(Queue::JobCounterExtension)
|
41
|
+
Sidekiq::JobSet.prepend(JobSet::JobCounterExtension)
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
def change_count(job, change_by)
|
46
|
+
unless [-1, 1].include?(change_by)
|
47
|
+
fail("Unsupported change_by value: #{change_by}")
|
48
|
+
end
|
49
|
+
|
50
|
+
job_key = SidekiqUtils::RedisMonitorStorage.job_prefix(
|
51
|
+
job, unwrap_arguments: true)
|
52
|
+
hash_key = {
|
53
|
+
queue: job['queue'],
|
54
|
+
job: job_key,
|
55
|
+
}.to_json
|
56
|
+
|
57
|
+
LOCK.synchronize do
|
58
|
+
@counts_to_flush ||= {}
|
59
|
+
@counts_to_flush[hash_key] ||= 0
|
60
|
+
@counts_to_flush[hash_key] += change_by
|
61
|
+
|
62
|
+
if !Rails.env.test?
|
63
|
+
@sync_thread ||= Thread.new do
|
64
|
+
sleep SYNC_COUNTS_EVERY
|
65
|
+
sync_to_redis
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
sync_to_redis if Rails.env.test?
|
71
|
+
end
|
72
|
+
|
73
|
+
def sync_to_redis
|
74
|
+
local_counts_to_flush = nil
|
75
|
+
LOCK.synchronize do
|
76
|
+
local_counts_to_flush = @counts_to_flush
|
77
|
+
@counts_to_flush = {}
|
78
|
+
@sync_thread = nil
|
79
|
+
end
|
80
|
+
(local_counts_to_flush || {}).each do |hash_key, change_by|
|
81
|
+
Sidekiq.redis do |redis|
|
82
|
+
count = redis.hincrby(REDIS_KEY, hash_key, change_by)
|
83
|
+
if count < 0 && change_by < 0
|
84
|
+
# this shouldn't happen, but it could when we first deploy this.
|
85
|
+
# just makes sure we don't end up with negative values here
|
86
|
+
# which don't make sense
|
87
|
+
#
|
88
|
+
# we only ever increment by the same amount we just did because
|
89
|
+
# we can't be responsible for more of a discrepancy at this
|
90
|
+
# point and we don't want multiple threads to overcorrect for
|
91
|
+
# each other
|
92
|
+
count = redis.hincrby(REDIS_KEY, hash_key, -1*change_by)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
at_exit { SidekiqUtils::JobCounter.send(:sync_to_redis) }
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
class SortedEntry
|
102
|
+
module JobCounterExtension
|
103
|
+
def delete
|
104
|
+
if super
|
105
|
+
JobCounter.decrement(item)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def remove_job
|
112
|
+
super do |message|
|
113
|
+
JobCounter.decrement(Sidekiq.load_json(message))
|
114
|
+
yield message
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
class ScheduledSet
|
121
|
+
module JobCounterExtension
|
122
|
+
def delete
|
123
|
+
if super
|
124
|
+
JobCounter.decrement(item)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
class Job
|
131
|
+
module JobCounterExtension
|
132
|
+
def delete
|
133
|
+
super
|
134
|
+
JobCounter.decrement(item)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
class Queue
|
140
|
+
module JobCounterExtension
|
141
|
+
def clear
|
142
|
+
super
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
class JobSet
|
148
|
+
module JobCounterExtension
|
149
|
+
def clear
|
150
|
+
each(&:delete)
|
151
|
+
super
|
152
|
+
end
|
153
|
+
|
154
|
+
def delete_by_value(name, value)
|
155
|
+
if super
|
156
|
+
JobCounter.decrement(Sidekiq.load_json(value))
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|