sidekiq_utils 1.0.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 +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
|