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