sidekiq_autoscale 0.2.2

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
+ SHA256:
3
+ metadata.gz: db767100bab6dcb54dcea794f7a5fb3b4e1ba925330ece323350fd6256399899
4
+ data.tar.gz: 4bb7d27d12ec1953e3cfdc1b37cccabbde656e444abe8e6cf4ecafad838e9f94
5
+ SHA512:
6
+ metadata.gz: 4bd8d6c970aac521f6cfc52bcb4fc8834058ab2bb23e68bf903eb51e8480e0c3b40b69bda22955e1466985d77a57067b94916ba61b10220de1d3836a0c3dd70f
7
+ data.tar.gz: a4818632e8f52a4955beb4014f113f3b5f6f64f38c9f6503229a60b97fae38e77361b99fb1297b8590e62fcacc395f9f9fe570357a46f600ef31033bd72aca6e
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2019 Traction Guest
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,152 @@
1
+ [![Maintainability](https://api.codeclimate.com/v1/badges/505a50e09d651c0423fd/maintainability)](https://codeclimate.com/github/tractionguest/sidekiq_autoscaling/maintainability)
2
+
3
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/505a50e09d651c0423fd/test_coverage)](https://codeclimate.com/github/tractionguest/sidekiq_autoscaling/test_coverage)
4
+
5
+ # SidekiqAutoscale
6
+
7
+ A simple gem to manage autoscaling of Sidekiq worker pools
8
+
9
+ ## Usage
10
+
11
+
12
+
13
+ ## Installation
14
+ Add this line to your application's Gemfile:
15
+
16
+ ```ruby
17
+ gem 'sidekiq_autoscale'
18
+ ```
19
+
20
+ And then execute:
21
+ ```bash
22
+ $ bundle install
23
+ ```
24
+
25
+ Install the initializer:
26
+ ```bash
27
+ $ rails g sidekiq_autoscale:install
28
+ ```
29
+
30
+ This will create a `config/initializers/sidekiq_autoscale.rb` file.
31
+
32
+ ### Installing middleware in Sidekiq chain
33
+
34
+ Add the scaling middleware to your Sidekiq configuration
35
+
36
+ ```ruby
37
+ Sidekiq.configure_server do |config|
38
+ .....
39
+ config.server_middleware do |chain|
40
+ chain.add SidekiqScaling::Middleware
41
+ end
42
+ end
43
+
44
+ ```
45
+
46
+
47
+ ## Configuration
48
+
49
+ All configuration can be done in a `SidekiqAutoscale.configure` block, either in a standalone initializer or in `<environment>.rb` files.
50
+
51
+ Standard configuration looks like this:
52
+
53
+ ```ruby
54
+ SidekiqAutoscale.configure do |config|
55
+ config.cache = Rails.cache
56
+ config.logger = Rails.logger
57
+ config.redis = # Put a real Redis client instance here
58
+
59
+ # Number of workers will never go below this threshold
60
+ config.min_workers = 1
61
+
62
+ # Number of workers will never go above this threshold
63
+ config.max_workers = 20
64
+
65
+ # The up and down thresholds used by all scaling strategies
66
+ config.scale_down_threshold = 1.0
67
+ config.scale_up_threshold = 5.0
68
+
69
+ # Current strategies are:
70
+ # :oldest_job - scales based on the age (in seconds) of the oldest job in any Sidekiq queue
71
+ # :delay_based - scales based on the average age (in seconds) all jobs run in the last minute
72
+ # :linear - scales based the total number of jobs in all queues, divided by the number of workers
73
+ # :base - do not scale, ever
74
+ config.strategy = :base
75
+
76
+ # Current adapters are:
77
+ # :nil - scaling events do nothing
78
+ # :heroku - scale a Heroku dyno
79
+
80
+ config.adapter = :heroku
81
+
82
+ # Any configuration required for the selected adapter
83
+ # Heroku requires the following:
84
+ config.adapter_config = {
85
+ api_key: "HEROKU_API_KEY",
86
+ worker_dyno_name: "DYNO_WORKER_NAME",
87
+ app_name: "HEROKU_APP_NAME"
88
+ }
89
+ # Kubernetes requires the following:
90
+ config.adapter_config = {
91
+ deployment_name: "myapp-sidekiq",
92
+ }
93
+
94
+ # The minimum amount of time to wait between scaling events
95
+ # Useful to tweak based on how long it takes for a new worker
96
+ # to spin up and start working on the pool
97
+ # config.min_scaling_interval = 5.minutes.to_i
98
+
99
+ # The number of workers to change in a scaling event
100
+ # config.scale_by = 1
101
+
102
+ # This proc will be called on a scaling event
103
+ # config.on_scaling_event = Proc.new { |event| Rails.logger.info event.to_json }
104
+
105
+ # This proc will be called when a scaling event errors out
106
+ # By default, nothing happens
107
+ # config.on_scaling_error = Proc.new { |error| Rails.logger.error error.to_json }
108
+ end
109
+ ```
110
+
111
+ ## Kubernetes
112
+
113
+ This gem can scale a Kubernetes `Deployment` object that Sidekiq is running in.
114
+
115
+ Doing so requires the following RBAC config:
116
+
117
+ ```
118
+
119
+ ---
120
+ kind: Role
121
+ apiVersion: rbac.authorization.k8s.io/v1
122
+ metadata:
123
+ name: myapp-sidekiq
124
+ rules:
125
+ - apiGroups: ["apps"]
126
+ resources: ["deployments"]
127
+ verbs: ["get", "patch"]
128
+
129
+ ---
130
+ kind: RoleBinding
131
+ apiVersion: rbac.authorization.k8s.io/v1
132
+ metadata:
133
+ name: sidekiq
134
+ subjects:
135
+ - kind: ServiceAccount
136
+ name: myapp-sidekiq
137
+ roleRef:
138
+ kind: Role
139
+ name: myapp-sidekiq
140
+ apiGroup: rbac.authorization.k8s.io
141
+
142
+ ---
143
+ kind: ServiceAccount
144
+ apiVersion: v1
145
+ metadata:
146
+ name: myapp-sidekiq
147
+ ```
148
+
149
+ Then assign your sidekiq deployment the `myapp-sidekiq` service account.
150
+
151
+ ## License
152
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "bundler/setup"
5
+ rescue LoadError
6
+ puts "You must `gem install bundler` and `bundle install` to run rake tasks"
7
+ end
8
+
9
+ require "rdoc/task"
10
+
11
+ RDoc::Task.new(:rdoc) do |rdoc|
12
+ rdoc.rdoc_dir = "rdoc"
13
+ rdoc.title = "SidekiqAutoscale"
14
+ rdoc.options << "--line-numbers"
15
+ rdoc.rdoc_files.include("README.md")
16
+ rdoc.rdoc_files.include("lib/**/*.rb")
17
+ end
18
+
19
+ require "bundler/gem_tasks"
20
+
21
+ require "rake/testtask"
22
+
23
+ Rake::TestTask.new(:test) do |t|
24
+ t.libs << "test"
25
+ t.pattern = "test/**/*_test.rb"
26
+ t.verbose = false
27
+ end
28
+
29
+ task default: :test
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module SidekiqAutoscale
6
+ class InstallGenerator < Rails::Generators::Base
7
+ source_root File.expand_path("templates", __dir__)
8
+
9
+ def generate_install
10
+ template "sidekiq_autoscale_initializer_template.template", "config/initializers/sidekiq_autoscale.rb"
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ SidekiqAutoscale.configure do |config|
4
+ config.cache = Rails.cache
5
+ config.logger = Rails.logger
6
+ config.redis_client = # Put a real Redis client instance here
7
+
8
+ # Number of workers will never go below this threshold
9
+ config.min_workers = 1
10
+
11
+ # Number of workers will never go above this threshold
12
+ config.max_workers = 20
13
+
14
+ # The up and down thresholds used by all scaling strategies
15
+ config.scale_down_threshold = 1.0
16
+ config.scale_up_threshold = 5.0
17
+
18
+ # Current strategies are:
19
+ # :oldest_job - scales based on the age (in seconds) of the oldest job in any Sidekiq queue
20
+ # :delay_based - scales based on the average age (in seconds) all jobs run in the last minute
21
+ # :linear - scales based the total number of jobs in all queues, divided by the number of workers
22
+ # :base - do not scale, ever
23
+ config.strategy = :base
24
+
25
+ # Current adapters are:
26
+ # :nil - scaling events do nothing
27
+ # :heroku - scale a Heroku dyno
28
+
29
+ config.adapter = :heroku
30
+
31
+ # Any configuration required for the selected adapter
32
+ # Heroku requires the following:
33
+ config.adapter_config = {
34
+ api_key: "HEROKU_API_KEY",
35
+ worker_dyno_name: "DYNO_WORKER_NAME",
36
+ app_name: "HEROKU_APP_NAME"
37
+ }
38
+
39
+ # The minimum amount of time to wait between scaling events
40
+ # Useful to tweak based on how long it takes for a new worker
41
+ # to spin up and start working on the pool
42
+ # config.min_scaling_interval = 5.minutes.to_i
43
+
44
+ # The number of workers to change in a scaling event
45
+ # config.scale_by = 1
46
+
47
+ # This proc will be called on a scaling event
48
+ # config.on_scaling_event = Proc.new { |event| Rails.logger.info event.to_json }
49
+
50
+ # This proc will be called when a scaling event errors out
51
+ # By default, nothing happens
52
+ # config.on_scaling_error = Proc.new { |error| Rails.logger.error error.to_json }
53
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SidekiqAutoscale
4
+ class HerokuAdapter
5
+ def initialize
6
+ require "platform-api"
7
+ @app_name = SidekiqAutoscale.adapter_config[:app_name]
8
+ @dyno_name = SidekiqAutoscale.adapter_config[:worker_dyno_name]
9
+ @client = PlatformAPI.connect_oauth(SidekiqAutoscale.adapter_config[:api_key])
10
+ end
11
+
12
+ def worker_count
13
+ @client.formation.list(@app_name)
14
+ .select {|i| i["type"] == @dyno_name }
15
+ .map {|i| i["quantity"] }
16
+ .reduce(0, &:+)
17
+ rescue Excon::Errors::Error => e
18
+ SidekiqAutoscale.on_scaling_error(e)
19
+ 0
20
+ end
21
+
22
+ def worker_count=(val)
23
+ return if val == worker_count
24
+
25
+ SidekiqAutoscale.logger.info("[SIDEKIQ_SCALE][HEROKU_ACTION] Setting new worker count to #{val} (is currenly #{worker_count})")
26
+ @client.formation.update(@app_name, @dyno_name, quantity: val)
27
+ rescue Excon::Errors::Error, Heroku::API::Errors::Error => e
28
+ SidekiqAutoscale.on_scaling_error(e)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SidekiqAutoscale
4
+ # Scale a Kubernetes deployment object.
5
+ class KubernetesAdapter
6
+ def initialize
7
+ require "k8s-ruby"
8
+
9
+ namespace = File.read("/run/secrets/kubernetes.io/serviceaccount/namespace")
10
+ client = K8s::Client.autoconfig
11
+
12
+ @deployment_name = SidekiqAutoscale.adapter_config[:deployment_name]
13
+ @resources = client.api("apps/v1").resource("deployments", namespace: namespace)
14
+ end
15
+
16
+ def worker_count
17
+ @resources.get(@deployment_name).spec.replicas
18
+ rescue Excon::Errors::Error, K8s::Error, K8s::Error::Forbidden => e
19
+ SidekiqAutoscale.on_scaling_error(e)
20
+ 0
21
+ end
22
+
23
+ def worker_count=(val)
24
+ return if val == worker_count
25
+
26
+ SidekiqAutoscale.logger.info("[SIDEKIQ_SCALE][KUBERNETES_ACTION] Setting new worker count to #{val} (is currenly #{worker_count})")
27
+ @resources.merge_patch(@deployment_name, spec: {replicas: val})
28
+ rescue Excon::Errors::Error, K8s::Error, K8s::Error::Forbidden => e
29
+ SidekiqAutoscale.on_scaling_error(e)
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SidekiqAutoscale
4
+ class NilAdapter
5
+ def initialize
6
+ @sidekiq_adapter = ::SidekiqAutoscale::SidekiqInterface.new
7
+ end
8
+
9
+ def worker_count
10
+ @sidekiq_adapter.total_workers
11
+ end
12
+
13
+ def worker_count=(val)
14
+ SidekiqAutoscale.logger.debug("Attempting to autoscale sidekiq to #{val} workers")
15
+ end
16
+ end
17
+ end
File without changes
@@ -0,0 +1,201 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SidekiqAutoscale
4
+ module Config
5
+ module SharedConfigs
6
+ LOG_TAG = "[SIDEKIQ_SCALING]"
7
+
8
+ attr_writer :config
9
+
10
+ def config
11
+ @config ||= ActiveSupport::OrderedOptions.new
12
+ end
13
+
14
+ def strategy
15
+ config.strategy || :base
16
+ end
17
+
18
+ def strategy_klass
19
+ @strategy_klass ||= begin
20
+ known_strats = [
21
+ ::SidekiqAutoscale::Strategies::BaseScaling,
22
+ ::SidekiqAutoscale::Strategies::DelayScaling,
23
+ ::SidekiqAutoscale::Strategies::OldestJobScaling,
24
+ ::SidekiqAutoscale::Strategies::LinearScaling,
25
+ ::SidekiqAutoscale::Strategies::DynamicLatencyScaling
26
+
27
+ ]
28
+ strat_klass_name = known_strats.map(&:to_s).find {|i| i.end_with?("#{strategy.to_s.camelize}Scaling") }
29
+ if strat_klass_name.nil?
30
+ raise ::SidekiqAutoscale::Exception.new <<~LOG
31
+ #{LOG_TAG} Unknown scaling strategy: [#{strategy.to_s.camelize}Scaling]")
32
+ LOG
33
+ end
34
+
35
+ strat_klass_name.constantize.new
36
+ end
37
+ end
38
+
39
+ def adapter
40
+ config.adapter || :nil
41
+ end
42
+
43
+ def adapter_klass
44
+ @adapter_klass ||= begin
45
+ known_adapters = [::SidekiqAutoscale::NilAdapter,
46
+ ::SidekiqAutoscale::HerokuAdapter,
47
+ ::SidekiqAutoscale::KubernetesAdapter].freeze
48
+ adapter_klass_name = known_adapters.map(&:to_s).find {|i| i.end_with?("#{adapter.to_s.camelize}Adapter") }
49
+ if adapter_klass_name.nil?
50
+ raise ::SidekiqAutoscale::Exception.new("#{LOG_TAG} Unknown scaling adapter: [#{adapter.to_s.camelize}Adapter]")
51
+ end
52
+
53
+ adapter_klass_name.constantize.new
54
+ end
55
+ end
56
+
57
+ def adapter_config
58
+ config.adapter_config
59
+ end
60
+
61
+ def scale_up_threshold
62
+ (config.scale_up_threshold ||= begin
63
+ validate_scaling_thresholds
64
+ validated_scale_up_threshold
65
+ end).to_f
66
+ end
67
+
68
+ def scale_down_threshold
69
+ (config.scale_down_threshold ||= begin
70
+ validate_scaling_thresholds
71
+ validated_scale_down_threshold
72
+ end).to_f
73
+ end
74
+
75
+ def max_workers
76
+ (@max_workers ||= begin
77
+ validate_worker_set
78
+ validated_max_workers
79
+ end).to_i
80
+ end
81
+
82
+ def min_workers
83
+ (@min_workers ||= begin
84
+ validate_worker_set
85
+ validated_min_workers
86
+ end).to_i
87
+ end
88
+
89
+ def scale_by
90
+ (config.scale_by || ENV.fetch("SIDEKIQ_AUTOSCALE_SCALE_BY", 1)).to_i
91
+ end
92
+
93
+ def min_scaling_interval
94
+ (config.min_scaling_interval || 5.minutes).to_i
95
+ end
96
+
97
+ def redis_client
98
+ raise ::SidekiqAutoscale::Exception.new("No Redis client defined") unless config.redis_client
99
+
100
+ config.redis_client
101
+ end
102
+
103
+ def logger
104
+ config.logger ||= Rails.logger
105
+ end
106
+
107
+ def cache
108
+ config.cache ||= ActiveSupport::Cache::NullStore.new
109
+ end
110
+
111
+ def on_scaling_error(e)
112
+ logger.error(e)
113
+ return unless config.on_scaling_error.respond_to?(:call)
114
+
115
+ config.on_scaling_error.call(e)
116
+ end
117
+
118
+ def on_scaling_event(event)
119
+ details = config.to_h.slice(:strategy,
120
+ :adapter,
121
+ :scale_up_threshold,
122
+ :scale_down_threshold,
123
+ :max_workers,
124
+ :min_workers,
125
+ :scale_by,
126
+ :min_scaling_interval)
127
+
128
+ on_head_bump(details.merge(event)) if event[:target_workers] == max_workers
129
+ on_toe_stub(details.merge(event)) if event[:target_workers] == min_workers
130
+
131
+ return unless config.on_scaling_event.respond_to?(:call)
132
+
133
+ config.on_scaling_event.call(details.merge(event))
134
+ end
135
+
136
+ def on_head_bump(event)
137
+ return unless config.on_head_bump.respond_to?(:call)
138
+
139
+ config.on_head_bump.call(event)
140
+ end
141
+
142
+ def on_toe_stub(event)
143
+ return unless config.on_toe_stub.respond_to?(:call)
144
+
145
+ config.on_toe_stub.call(event)
146
+ end
147
+
148
+ def sidekiq_interface
149
+ @sidekiq_interface ||= ::SidekiqAutoscale::SidekiqInterface.new
150
+ end
151
+
152
+ def lock_manager
153
+ config.lock_manager ||= ::Redlock::Client.new(Array.wrap(redis_client),
154
+ retry_count: 3,
155
+ retry_delay: 200,
156
+ retry_jitter: 50,
157
+ redis_timeout: 0.1)
158
+ end
159
+
160
+ def lock_time
161
+ config.lock_time || 5_000
162
+ end
163
+
164
+ private
165
+
166
+ def validate_worker_set
167
+ ex_klass = ::SidekiqAutoscale::Exception
168
+ raise ex_klass.new("No max workers set") unless validated_max_workers.positive?
169
+ raise ex_klass.new("No min workers set") unless validated_min_workers.positive?
170
+ if validated_max_workers < validated_min_workers
171
+ raise ex_klass.new("Max workers must be higher than min workers")
172
+ end
173
+ end
174
+
175
+ def validate_scaling_thresholds
176
+ ex_klass = ::SidekiqAutoscale::Exception
177
+ raise ex_klass.new("No scale up threshold set") unless validated_scale_up_threshold.positive?
178
+ raise ex_klass.new("No scale down threshold set") unless validated_scale_down_threshold.positive?
179
+ if validated_scale_up_threshold < validated_scale_down_threshold
180
+ raise ex_klass.new("Scale up threshold must be higher than scale down threshold")
181
+ end
182
+ end
183
+
184
+ def validated_scale_up_threshold
185
+ (config.scale_up_threshold || ENV.fetch("SIDEKIQ_AUTOSCALE_UP_THRESHOLD", 5.0)).to_f
186
+ end
187
+
188
+ def validated_scale_down_threshold
189
+ (config.scale_down_threshold || ENV.fetch("SIDEKIQ_AUTOSCALE_DOWN_THRESHOLD", 1.0)).to_f
190
+ end
191
+
192
+ def validated_max_workers
193
+ (config.max_workers || ENV.fetch("SIDEKIQ_AUTOSCALE_MAX_WORKERS", 10)).to_i
194
+ end
195
+
196
+ def validated_min_workers
197
+ (config.min_workers || ENV.fetch("SIDEKIQ_AUTOSCALE_MIN_WORKERS", 1)).to_i
198
+ end
199
+ end
200
+ end
201
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SidekiqAutoscale
4
+ class Exception < ::StandardError
5
+ end
6
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SidekiqAutoscale
4
+ class Middleware
5
+ LAST_SCALED_AT_EVENT_KEY = "sidekiq_autoscaling:last_scaled_at"
6
+ SCALING_LOCK_KEY = "sidekiq_autoscaling:scaling_lock"
7
+ LOG_TAG = "[SIDEKIQ_SCALE][SCALING_EVENT]"
8
+ WORKER_COUNT_KEY = "sidekiq_autoscaling/current_worker_count"
9
+
10
+ # @param [Object] worker the worker instance
11
+ # @param [Hash] job the full job payload
12
+ # * @see https://github.com/mperham/sidekiq/wiki/Job-Format
13
+ # @param [String] queue the name of the queue the job was pulled from
14
+ # @yield the next middleware in the chain or worker `perform` method
15
+ # @return [Void]
16
+ def call(_worker_class, job, _queue)
17
+ # In case the scaling strategy needs to record job-specific stuff before it runs
18
+ SidekiqAutoscale.strategy_klass.log_job(job)
19
+ yield # Run the job, THEN scale the cluster
20
+ begin
21
+ return unless SidekiqAutoscale.strategy_klass.workload_change_needed?(job)
22
+
23
+ direction = SidekiqAutoscale.strategy_klass.scaling_direction(job)
24
+ new_worker_count = worker_count + (SidekiqAutoscale.scale_by * direction)
25
+
26
+ set_worker_count(new_worker_count, event_id: job["jid"], direction: direction)
27
+ rescue StandardError => e
28
+ SidekiqAutoscale.logger.error(e)
29
+ SidekiqAutoscale.on_scaling_error(e)
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def worker_count
36
+ SidekiqAutoscale.cache.fetch(WORKER_COUNT_KEY, expires_in: 1.minute) do
37
+ SidekiqAutoscale.adapter_klass.worker_count
38
+ end
39
+ end
40
+
41
+ def set_worker_count(n, event_id: SecureRandom.hex, direction:)
42
+ clamped = n.clamp(SidekiqAutoscale.min_workers, SidekiqAutoscale.max_workers)
43
+
44
+ SidekiqAutoscale.lock_manager.lock(SCALING_LOCK_KEY, SidekiqAutoscale.lock_time) do |locked|
45
+ # Not awesome, but gotta handle the initial nil case
46
+ last_scaled_at = SidekiqAutoscale.redis_client.get(LAST_SCALED_AT_EVENT_KEY).to_f
47
+ SidekiqAutoscale.logger.debug <<~LOG
48
+ #{LOG_TAG}[#{event_id}] Concurrency lock obtained: #{locked}"
49
+ Last scaled [#{Time.current.to_i - last_scaled_at.to_i}] seconds ago"
50
+ Scaling every [#{SidekiqAutoscale.min_scaling_interval}] seconds"
51
+ LOG
52
+
53
+ if locked && (last_scaled_at < SidekiqAutoscale.min_scaling_interval.seconds.ago.to_f)
54
+ SidekiqAutoscale.adapter_klass.worker_count = clamped
55
+ SidekiqAutoscale.cache.delete(WORKER_COUNT_KEY)
56
+ SidekiqAutoscale.redis_client.set(LAST_SCALED_AT_EVENT_KEY, Time.current.to_f)
57
+ SidekiqAutoscale.on_scaling_event(
58
+ direction: direction,
59
+ target_workers: clamped,
60
+ event_id: event_id,
61
+ current_worker_count: worker_count,
62
+ last_scaled_at: last_scaled_at
63
+ )
64
+ else
65
+ SidekiqAutoscale.logger.debug("#{LOG_TAG}[#{event_id}] ***NOT SCALING***")
66
+ end
67
+
68
+ SidekiqAutoscale.logger.debug("#{LOG_TAG}[#{event_id}] RELEASING LOCK #{locked}") if locked
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails"
4
+
5
+ module SidekiqAutoscale
6
+ class Railtie < ::Rails::Railtie
7
+ config.sidekiq_autoscale = ActiveSupport::OrderedOptions.new
8
+
9
+ config.after_initialize do
10
+ SidekiqAutoscale.logger.info <<~LOG
11
+ [SIDEKIQ_SCALE] Scaling strategy: #{SidekiqAutoscale.strategy}
12
+ [SIDEKIQ_SCALE] Min workers: #{SidekiqAutoscale.min_workers}
13
+ [SIDEKIQ_SCALE] Max workers: #{SidekiqAutoscale.max_workers}
14
+ [SIDEKIQ_SCALE] Scaling by: #{SidekiqAutoscale.scale_by}
15
+ [SIDEKIQ_SCALE] Provider adapter: #{SidekiqAutoscale.adapter}
16
+ LOG
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq/api"
4
+
5
+ module SidekiqAutoscale
6
+ class SidekiqInterface
7
+ def total_queue_size
8
+ queue_names.map {|q| ::Sidekiq::Queue.new(q).size }.reduce(0, &:+)
9
+ end
10
+
11
+ def queue_names
12
+ ::Sidekiq::Queue.all.map(&:name)
13
+ end
14
+
15
+ def busy_threads
16
+ ::Sidekiq::Workers.new.map {|_, thread, _| thread }.uniq.size
17
+ end
18
+
19
+ def latency
20
+ queue_names.map {|q| ::Sidekiq::Queue.new(q).latency }.max
21
+ end
22
+
23
+ def total_workers
24
+ process_set.size
25
+ end
26
+
27
+ def total_threads
28
+ process_set.map {|w| w["concurrency"] }.reduce(0, &:+)
29
+ end
30
+
31
+ def available_threads
32
+ total_threads - busy_threads
33
+ end
34
+
35
+ def youngest_worker
36
+ process_set.map {|w| w["started_at"] }.max
37
+ end
38
+
39
+ private
40
+
41
+ def process_set
42
+ @process_set ||= ::Sidekiq::ProcessSet.new
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SidekiqAutoscale
4
+ module Strategies
5
+ class BaseScaling
6
+ # This strategy doesn't care about individual job metrics
7
+ def log_job(_job); end
8
+
9
+ def workload_change_needed?(_job)
10
+ false
11
+ end
12
+
13
+ def scaling_direction(_job)
14
+ 0
15
+ end
16
+
17
+ private
18
+
19
+ def scale_up_threshold
20
+ SidekiqAutoscale.scale_up_threshold
21
+ end
22
+
23
+ def scale_down_threshold
24
+ SidekiqAutoscale.scale_down_threshold
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SidekiqAutoscale
4
+ module Strategies
5
+ class DelayScaling < BaseScaling
6
+ SAMPLE_RANGE = 60
7
+ DELAY_LOG_KEY = "sidekiq_autoscaling:delay_log"
8
+ DELAY_AVERAGE_CACHE_KEY = "sidekiq_autoscaling:delay_average"
9
+ LOG_TAG = "[SIDEKIQ_SCALE][DELAY_SCALING]"
10
+
11
+ def log_job(job)
12
+ timestamp = Time.current.to_f
13
+
14
+ # Gotta do it this way so that each entry is guaranteed to be unique
15
+ zset_payload = {delay: (timestamp - job["enqueued_at"]), jid: job["jid"]}.to_json
16
+
17
+ # Redis zadd runs in O(log(N)) time, so this should be threaded to avoid blocking
18
+ # Also, it should be connection-pooled, but I can't remember if we're using
19
+ # redis connection pooling anywhere
20
+ Thread.new {
21
+ SidekiqAutoscale.redis_client.zadd(DELAY_LOG_KEY, timestamp, zset_payload)
22
+ }
23
+ end
24
+
25
+ def workload_change_needed?(_job)
26
+ workload_too_high? || workload_too_low?
27
+ end
28
+
29
+ def scaling_direction(_job)
30
+ return -1 if workload_too_low?
31
+ return 1 if workload_too_high?
32
+
33
+ 0
34
+ end
35
+
36
+ private
37
+
38
+ def delay_average
39
+ # Only calculate this once every minute - this operation isn't very efficient
40
+ # We may want to offload it to another Redis DB number, which will be just delay keys
41
+ SidekiqAutoscale.cache.fetch(DELAY_AVERAGE_CACHE_KEY, expires_in: SAMPLE_RANGE) do
42
+ # Delete old scores that won't be included in the metric
43
+ SidekiqAutoscale.redis_client.zremrangebyscore(DELAY_LOG_KEY, 0, SAMPLE_RANGE.ago.to_f)
44
+ vals = SidekiqAutoscale.redis_client.zrange(DELAY_LOG_KEY, 0, -1).map {|i| JSON.parse(i)["delay"].to_f }
45
+ return 0 if vals.empty?
46
+
47
+ vals.instance_eval { reduce(:+) / size.to_f }
48
+ rescue JSON::ParserError => e
49
+ SidekiqAutoscale.logger.error(e)
50
+ SidekiqAutoscale.logger.error(e.backtrace.join("\n"))
51
+ return 0
52
+ end
53
+ end
54
+
55
+ def workload_too_high?
56
+ too_high = delay_average > SidekiqAutoscale.scale_up_threshold
57
+ SidekiqAutoscale.logger.debug("#{LOG_TAG} Workload too high") if too_high
58
+ SidekiqAutoscale.logger.debug("#{LOG_TAG} Current average delay: #{delay_average}, max allowed: #{SidekiqAutoscale.scale_up_threshold}")
59
+ too_high
60
+ end
61
+
62
+ def workload_too_low?
63
+ too_low = delay_average < SidekiqAutoscale.scale_down_threshold
64
+ SidekiqAutoscale.logger.debug("#{LOG_TAG} Workload too low") if too_low
65
+ SidekiqAutoscale.logger.debug("#{LOG_TAG} Current average delay: #{delay_average}, min allowed: #{SidekiqAutoscale.scale_down_threshold}")
66
+ too_low
67
+ end
68
+
69
+ def delay_array; end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SidekiqAutoscale
4
+ module Strategies
5
+ class DynamicLatencyScaling < BaseScaling
6
+ LOG_TAG = "[SIDEKIQ_SCALE][DYNAMIC_LATENCY_SCALING]"
7
+ def workload_change_needed?(_job)
8
+ workload_too_high? || workload_too_low?
9
+ end
10
+
11
+ def scaling_direction(_job)
12
+ return -1 if workload_too_low?
13
+ return [scale_up_factor.to_i, 1].max if workload_too_high?
14
+
15
+ 0
16
+ end
17
+
18
+ private
19
+
20
+ def scale_up_factor
21
+ 1 + (latency - scale_up_threshold) / dynamic_multiple_base
22
+ end
23
+
24
+ def dynamic_multiple_base
25
+ @dynamic_multiple_base ||= scale_up_threshold - scale_down_threshold
26
+ end
27
+
28
+ def workload_too_high?
29
+ too_high = latency > scale_up_threshold
30
+ SidekiqAutoscale.logger.debug("#{LOG_TAG} Workload too high") if too_high
31
+ SidekiqAutoscale.logger.debug("#{LOG_TAG} Current average delay: #{latency}, max allowed: #{scale_up_threshold}")
32
+ too_high
33
+ end
34
+
35
+ def workload_too_low?
36
+ too_low = latency < scale_down_threshold
37
+ SidekiqAutoscale.logger.debug("#{LOG_TAG} Workload too low") if too_low
38
+ SidekiqAutoscale.logger.debug("#{LOG_TAG} Current average delay: #{latency}, min allowed: #{scale_down_threshold}")
39
+ too_low
40
+ end
41
+
42
+ def latency
43
+ SidekiqAutoscale.sidekiq_interface.latency
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SidekiqAutoscale
4
+ module Strategies
5
+ class LinearScaling < BaseScaling
6
+ LOG_TAG = "[SIDEKIQ_SCALE][LINEAR_SCALING]"
7
+
8
+ def workload_change_needed?(_job)
9
+ workload_too_high? || workload_too_low?
10
+ end
11
+
12
+ def scaling_direction(_job)
13
+ return 1 if workload_too_high?
14
+ return -1 if workload_too_low?
15
+
16
+ 0
17
+ end
18
+
19
+ private
20
+
21
+ # Remove available threads from total queue size in case there's pending
22
+ # tasks that are still spinning up,
23
+ def scheduled_jobs_per_thread
24
+ ((SidekiqAutoscale.sidekiq_interface.total_queue_size - SidekiqAutoscale.sidekiq_interface.available_threads).to_f / SidekiqAutoscale.sidekiq_interface.total_threads.to_f)
25
+ end
26
+
27
+ def workload_too_high?
28
+ too_high = scheduled_jobs_per_thread > SidekiqAutoscale.scale_up_threshold
29
+ if too_high
30
+ SidekiqAutoscale.logger.debug("#{LOG_TAG} Workload too low [Scheduled: #{scheduled_jobs_per_thread}, Max: #{SidekiqAutoscale.scale_up_threshold}]")
31
+ end
32
+ too_high
33
+ end
34
+
35
+ def workload_too_low?
36
+ too_low = scheduled_jobs_per_thread < SidekiqAutoscale.scale_down_threshold
37
+ if too_low
38
+ SidekiqAutoscale.logger.debug("#{LOG_TAG} Workload too low [Scheduled: #{scheduled_jobs_per_thread}, Min: #{SidekiqAutoscale.scale_down_threshold}]")
39
+ end
40
+ too_low
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SidekiqAutoscale
4
+ module Strategies
5
+ class OldestJobScaling < BaseScaling
6
+ LOG_TAG = "[SIDEKIQ_SCALE][OLDEST_JOB_SCALING]"
7
+ def workload_change_needed?(_job)
8
+ workload_too_high? || workload_too_low?
9
+ end
10
+
11
+ def scaling_direction(_job)
12
+ return -1 if workload_too_low?
13
+ return 1 if workload_too_high?
14
+
15
+ 0
16
+ end
17
+
18
+ private
19
+
20
+ def workload_too_high?
21
+ too_high = SidekiqAutoscale.sidekiq_interface.latency > SidekiqAutoscale.scale_up_threshold
22
+
23
+ SidekiqAutoscale.logger.debug("#{LOG_TAG} Workload too high") if too_high
24
+ SidekiqAutoscale.logger.debug("#{LOG_TAG} Current average delay: #{SidekiqAutoscale.sidekiq_interface.latency}, max allowed: #{SidekiqAutoscale.scale_up_threshold}")
25
+ too_high
26
+ end
27
+
28
+ def workload_too_low?
29
+ too_low = SidekiqAutoscale.sidekiq_interface.latency < SidekiqAutoscale.scale_down_threshold
30
+ SidekiqAutoscale.logger.debug("#{LOG_TAG} Workload too low") if too_low
31
+ SidekiqAutoscale.logger.debug("#{LOG_TAG} Current average delay: #{SidekiqAutoscale.sidekiq_interface.latency}, min allowed: #{SidekiqAutoscale.scale_down_threshold}")
32
+ too_low
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SidekiqAutoscale
4
+ VERSION = "0.2.2"
5
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "redlock"
4
+ require "sidekiq/api"
5
+ require "active_support/all"
6
+
7
+ require "sidekiq_autoscale/railtie"
8
+ require "sidekiq_autoscale/exception"
9
+ require "sidekiq_autoscale/sidekiq_interface"
10
+ require "sidekiq_autoscale/strategies/base_scaling"
11
+ require "sidekiq_autoscale/strategies/delay_scaling"
12
+ require "sidekiq_autoscale/strategies/linear_scaling"
13
+ require "sidekiq_autoscale/strategies/oldest_job_scaling"
14
+ require "sidekiq_autoscale/strategies/dynamic_latency_scaling"
15
+ require "sidekiq_autoscale/adapters/nil_adapter"
16
+ require "sidekiq_autoscale/adapters/heroku_adapter"
17
+ require "sidekiq_autoscale/adapters/kubernetes_adapter"
18
+ require "sidekiq_autoscale/middleware"
19
+ require "sidekiq_autoscale/config/callbacks"
20
+ require "sidekiq_autoscale/config/shared_configs"
21
+
22
+ module SidekiqAutoscale
23
+ class << self
24
+ include SidekiqAutoscale::Config::SharedConfigs
25
+
26
+ def configure
27
+ yield config
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+ # desc "Explaining what the task does"
3
+ # task :sidekiq_autoscale do
4
+ # # Task goes here
5
+ # end
metadata ADDED
@@ -0,0 +1,365 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sidekiq_autoscale
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.2
5
+ platform: ruby
6
+ authors:
7
+ - Steven Allen
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-03-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: addressable
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.8'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.8'
27
+ - !ruby/object:Gem::Dependency
28
+ name: k8s-ruby
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0.12'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0.12'
41
+ - !ruby/object:Gem::Dependency
42
+ name: platform-api
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.2'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.2'
55
+ - !ruby/object:Gem::Dependency
56
+ name: railties
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '6'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '6'
69
+ - !ruby/object:Gem::Dependency
70
+ name: redlock
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1'
83
+ - !ruby/object:Gem::Dependency
84
+ name: sidekiq
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '6'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '6'
97
+ - !ruby/object:Gem::Dependency
98
+ name: thor
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0.19'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0.19'
111
+ - !ruby/object:Gem::Dependency
112
+ name: awesome_print
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: bundler
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '2'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '2'
139
+ - !ruby/object:Gem::Dependency
140
+ name: byebug
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: guard
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '2'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '2'
167
+ - !ruby/object:Gem::Dependency
168
+ name: guard-bundler
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '2'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '2'
181
+ - !ruby/object:Gem::Dependency
182
+ name: guard-rspec
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - "~>"
186
+ - !ruby/object:Gem::Version
187
+ version: '4.7'
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - "~>"
193
+ - !ruby/object:Gem::Version
194
+ version: '4.7'
195
+ - !ruby/object:Gem::Dependency
196
+ name: mock_redis
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - ">="
200
+ - !ruby/object:Gem::Version
201
+ version: '0'
202
+ type: :development
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - ">="
207
+ - !ruby/object:Gem::Version
208
+ version: '0'
209
+ - !ruby/object:Gem::Dependency
210
+ name: rspec
211
+ requirement: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - ">="
214
+ - !ruby/object:Gem::Version
215
+ version: '3.2'
216
+ - - "<"
217
+ - !ruby/object:Gem::Version
218
+ version: '4'
219
+ type: :development
220
+ prerelease: false
221
+ version_requirements: !ruby/object:Gem::Requirement
222
+ requirements:
223
+ - - ">="
224
+ - !ruby/object:Gem::Version
225
+ version: '3.2'
226
+ - - "<"
227
+ - !ruby/object:Gem::Version
228
+ version: '4'
229
+ - !ruby/object:Gem::Dependency
230
+ name: rubocop
231
+ requirement: !ruby/object:Gem::Requirement
232
+ requirements:
233
+ - - ">="
234
+ - !ruby/object:Gem::Version
235
+ version: '0.50'
236
+ type: :development
237
+ prerelease: false
238
+ version_requirements: !ruby/object:Gem::Requirement
239
+ requirements:
240
+ - - ">="
241
+ - !ruby/object:Gem::Version
242
+ version: '0.50'
243
+ - !ruby/object:Gem::Dependency
244
+ name: rubocop-rspec
245
+ requirement: !ruby/object:Gem::Requirement
246
+ requirements:
247
+ - - "~>"
248
+ - !ruby/object:Gem::Version
249
+ version: '1'
250
+ type: :development
251
+ prerelease: false
252
+ version_requirements: !ruby/object:Gem::Requirement
253
+ requirements:
254
+ - - "~>"
255
+ - !ruby/object:Gem::Version
256
+ version: '1'
257
+ - !ruby/object:Gem::Dependency
258
+ name: simplecov
259
+ requirement: !ruby/object:Gem::Requirement
260
+ requirements:
261
+ - - "~>"
262
+ - !ruby/object:Gem::Version
263
+ version: '0.16'
264
+ type: :development
265
+ prerelease: false
266
+ version_requirements: !ruby/object:Gem::Requirement
267
+ requirements:
268
+ - - "~>"
269
+ - !ruby/object:Gem::Version
270
+ version: '0.16'
271
+ - !ruby/object:Gem::Dependency
272
+ name: simplecov-console
273
+ requirement: !ruby/object:Gem::Requirement
274
+ requirements:
275
+ - - "~>"
276
+ - !ruby/object:Gem::Version
277
+ version: '0.4'
278
+ type: :development
279
+ prerelease: false
280
+ version_requirements: !ruby/object:Gem::Requirement
281
+ requirements:
282
+ - - "~>"
283
+ - !ruby/object:Gem::Version
284
+ version: '0.4'
285
+ - !ruby/object:Gem::Dependency
286
+ name: sqlite3
287
+ requirement: !ruby/object:Gem::Requirement
288
+ requirements:
289
+ - - ">="
290
+ - !ruby/object:Gem::Version
291
+ version: '0'
292
+ type: :development
293
+ prerelease: false
294
+ version_requirements: !ruby/object:Gem::Requirement
295
+ requirements:
296
+ - - ">="
297
+ - !ruby/object:Gem::Version
298
+ version: '0'
299
+ - !ruby/object:Gem::Dependency
300
+ name: webmock
301
+ requirement: !ruby/object:Gem::Requirement
302
+ requirements:
303
+ - - ">="
304
+ - !ruby/object:Gem::Version
305
+ version: '0'
306
+ type: :development
307
+ prerelease: false
308
+ version_requirements: !ruby/object:Gem::Requirement
309
+ requirements:
310
+ - - ">="
311
+ - !ruby/object:Gem::Version
312
+ version: '0'
313
+ description: A simple gem to handle Sidekiq autoscaling.
314
+ email:
315
+ - sallen@tractionguest.com
316
+ executables: []
317
+ extensions: []
318
+ extra_rdoc_files: []
319
+ files:
320
+ - MIT-LICENSE
321
+ - README.md
322
+ - Rakefile
323
+ - lib/generators/sidekiq_autoscale/install/install_generator.rb
324
+ - lib/generators/sidekiq_autoscale/install/templates/sidekiq_autoscale_initializer_template.template
325
+ - lib/sidekiq_autoscale.rb
326
+ - lib/sidekiq_autoscale/adapters/heroku_adapter.rb
327
+ - lib/sidekiq_autoscale/adapters/kubernetes_adapter.rb
328
+ - lib/sidekiq_autoscale/adapters/nil_adapter.rb
329
+ - lib/sidekiq_autoscale/config/callbacks.rb
330
+ - lib/sidekiq_autoscale/config/shared_configs.rb
331
+ - lib/sidekiq_autoscale/exception.rb
332
+ - lib/sidekiq_autoscale/middleware.rb
333
+ - lib/sidekiq_autoscale/railtie.rb
334
+ - lib/sidekiq_autoscale/sidekiq_interface.rb
335
+ - lib/sidekiq_autoscale/strategies/base_scaling.rb
336
+ - lib/sidekiq_autoscale/strategies/delay_scaling.rb
337
+ - lib/sidekiq_autoscale/strategies/dynamic_latency_scaling.rb
338
+ - lib/sidekiq_autoscale/strategies/linear_scaling.rb
339
+ - lib/sidekiq_autoscale/strategies/oldest_job_scaling.rb
340
+ - lib/sidekiq_autoscale/version.rb
341
+ - lib/tasks/sidekiq_autoscale_tasks.rake
342
+ homepage: https://github.com/tractionguest/sidekiq_autoscaling
343
+ licenses:
344
+ - MIT
345
+ metadata: {}
346
+ post_install_message:
347
+ rdoc_options: []
348
+ require_paths:
349
+ - lib
350
+ required_ruby_version: !ruby/object:Gem::Requirement
351
+ requirements:
352
+ - - ">="
353
+ - !ruby/object:Gem::Version
354
+ version: '0'
355
+ required_rubygems_version: !ruby/object:Gem::Requirement
356
+ requirements:
357
+ - - ">="
358
+ - !ruby/object:Gem::Version
359
+ version: '0'
360
+ requirements: []
361
+ rubygems_version: 3.2.32
362
+ signing_key:
363
+ specification_version: 4
364
+ summary: A simple gem to handle Sidekiq autoscaling.
365
+ test_files: []