sidekiq_autoscale 0.2.2

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
+ 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: []