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 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