sidekiq-amigo 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 70d47770af144a84aedec89e8b74e4ebd13851119a5c329a0f4c65d56ec94a60
4
- data.tar.gz: 69305abac1bb5eeb00eaf81613e591476bf1acb35cdac50dcc97a094d255aab8
3
+ metadata.gz: c51def3cb58812e0889c0768708747aefd3956495692ffcbb6521a1d24c6d0b0
4
+ data.tar.gz: 5081debc483ce040e8c6f3a26cfacdd85094c5e70895b390f23f58f795663da3
5
5
  SHA512:
6
- metadata.gz: adf9aa76c352c9ddb9832f142d415b4efded423324dd78a87cfc3140a755eb266dd1a1b80e15a2f911ccafaaf8ea89f416d66dfe25a17955709fb1e4828554ef
7
- data.tar.gz: f69c16bfd06a7ea5cc3ac15db4a962f3fb02345db60ac6f581123bf5425627775bb1958e9f99ff0be8b5956db57d4c6fec64d5422ff7903c0157bf0a0f8ba4d1
6
+ metadata.gz: b126004a168ea18f4842bd34268fc3edb263d21de40731d178e9bd4a5db64bf403da0953b6083ea0e5c5e8d32bc9aa4faf0e66e54f0f166a077780d6573565f6
7
+ data.tar.gz: 4a710ac1029e607ed60b1f448e4c443d287bcb599252801a70a17a9478e0f5c80b373f610a14034be1d2d3c98541512e64ab2f8b2bc85869b70fbc009308007d
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "platform-api"
4
+
5
+ require "amigo/autoscaler"
6
+
7
+ module Amigo
8
+ class Autoscaler
9
+ # Autoscaler to use on Heroku, that starts additional worker processes when there is a high latency event
10
+ # and scales them down after the event is finished.
11
+ #
12
+ # When the first call of a high latency event happens (depth: 1), this class
13
+ # will ask Heroku how many dynos are in the formation. This is known as +active_event_initial_workers+.
14
+ #
15
+ # If +active_event_initial_workers+ is 0, no autoscaling will be done.
16
+ # This avoids a situation where a high latency event is triggered
17
+ # due to workers being deprovisioned intentionally, perhaps for maintenance.
18
+ #
19
+ # Each time the alert fires (see +Amigo::Autoscaler#alert_interval+),
20
+ # an additional worker will be added to the formation, up to +max_additional_workers+.
21
+ # So with +active_event_initial_workers+ of 1 and +max_additional_workers+ of 2,
22
+ # the first time the alert times, the formation will be set to 2 workers.
23
+ # The next time, it'll be set to 3 workers.
24
+ # After that, no additional workers will be provisioned.
25
+ #
26
+ # After the high latency event resolves,
27
+ # the dyno formation is restored to +active_event_initial_workers+.
28
+ #
29
+ # To use:
30
+ #
31
+ # heroku = PlatformAPI.connect_oauth(heroku_oauth_token)
32
+ # heroku_scaler = Amigo::Autoscaler::Heroku.new(heroku:, default_workers: 1)
33
+ # Amigo::Autoscaler.new(
34
+ # handlers: [heroku_scaler.alert_callback],
35
+ # latency_restored_handlers: [heroku_scaler.restored_callback],
36
+ # )
37
+ #
38
+ # See instance attributes for additional options.
39
+ #
40
+ # Note that this class is provided as an example, and potentially a base or implementation class.
41
+ # Your actual implementation may also want to alert when a max depth or duration is reached,
42
+ # since it can indicate a bigger problem. Autoscaling, especially of workers, is a tough problem
43
+ # without a one-size-fits-all approach.
44
+ class Heroku
45
+ # Heroku client, usually created via PlatformAPI.oauth_connect.
46
+ # @return [PlatformAPI::Client]
47
+ attr_reader :heroku
48
+
49
+ # Captured at the start of a high latency event.
50
+ # Nil otherwise.
51
+ # @return [Integer]
52
+ attr_reader :active_event_initial_workers
53
+
54
+ # Maximum number of workers to add.
55
+ #
56
+ # As the 'depth' of the alert is increased,
57
+ # workers are added to the recorded worker count until the max is reached.
58
+ # By default, this is 2 (so the max workers will be the recorded number, plus 2).
59
+ # Do not set this too high, since it can for example exhaust database connections or just end up
60
+ # increasing load.
61
+ #
62
+ # See class docs for more information.
63
+ # @return [Integer]
64
+ attr_reader :max_additional_workers
65
+
66
+ # Defaults to HEROKU_APP_NAME, which should already be set if you use Heroku dyna metadata,
67
+ # as per https://devcenter.heroku.com/articles/dyno-metadata.
68
+ # This must be provided if the env var is missing.
69
+ # @return [String]
70
+ attr_reader :app_id_or_app_name
71
+
72
+ # Defaults to 'worker', which is what you'll probably use if you have a simple system.
73
+ # If you use multiple worker processes for different queues, this class probably isn't sufficient.
74
+ # You will probably need to look at the slow queue names and determine the formation name to scale up.
75
+ # @return [String]
76
+ attr_reader :formation_id_or_formation_type
77
+
78
+ def initialize(
79
+ heroku:,
80
+ max_additional_workers: 2,
81
+ app_id_or_app_name: ENV.fetch("HEROKU_APP_NAME"),
82
+ formation_id_or_formation_type: "worker"
83
+ )
84
+
85
+ @heroku = heroku
86
+ @max_additional_workers = max_additional_workers
87
+ @app_id_or_app_name = app_id_or_app_name
88
+ @formation_id_or_formation_type = formation_id_or_formation_type
89
+ # Is nil outside of a latency event, set during a latency event. So if this is initialized to non-nil,
90
+ # we're already in a latency event.
91
+ @active_event_initial_workers = Sidekiq.redis do |r|
92
+ v = r.get("#{namespace}/active_event_initial_workers")
93
+ v&.to_i
94
+ end
95
+ end
96
+
97
+ def alert_callback
98
+ self.method(:scale_up)
99
+ end
100
+
101
+ def restored_callback
102
+ self.method(:scale_down)
103
+ end
104
+
105
+ protected def namespace
106
+ return "amigo/autoscaler/heroku"
107
+ end
108
+
109
+ # Potentially add another worker to the formation.
110
+ # @return [:noscale, :maxscale, :scaled] One of :noscale (no +active_event_initial_workers+),
111
+ # :maxscale (+max_additional_workers+ reached), or :scaled.
112
+ def scale_up(_queues_and_latencies, depth:, **)
113
+ # When the scaling event starts (or if this is the first time we've seen it
114
+ # but the event is already in progress), store how many workers we have.
115
+ # It needs to be stored in redis so it persists if
116
+ # the latency event continues through restarts.
117
+ if @active_event_initial_workers.nil?
118
+ @active_event_initial_workers = @heroku.formation.info(@app_id_or_app_name, @formation_id_or_formation_type).
119
+ fetch("quantity")
120
+ Sidekiq.redis do |r|
121
+ r.set("#{namespace}/active_event_initial_workers", @active_event_initial_workers.to_s)
122
+ end
123
+ end
124
+ return :noscale if @active_event_initial_workers.zero?
125
+ new_quantity = @active_event_initial_workers + depth
126
+ max_quantity = @active_event_initial_workers + @max_additional_workers
127
+ return :maxscale if new_quantity > max_quantity
128
+ @heroku.formation.update(@app_id_or_app_name, @formation_id_or_formation_type, {quantity: new_quantity})
129
+ return :scaled
130
+ end
131
+
132
+ # Reset the formation to +active_event_initial_workers+.
133
+ # @return [:noscale, :scaled] :noscale if +active_event_initial_workers+ is 0, otherwise :scaled.
134
+ def scale_down(**)
135
+ initial_workers = @active_event_initial_workers
136
+ Sidekiq.redis do |r|
137
+ r.del("#{namespace}/active_event_initial_workers")
138
+ end
139
+ @active_event_initial_workers = nil
140
+ return :noscale if initial_workers.zero?
141
+ @heroku.formation.update(@app_id_or_app_name, @formation_id_or_formation_type, {quantity: initial_workers})
142
+ return :scaled
143
+ end
144
+ end
145
+ end
146
+ end
@@ -6,20 +6,31 @@ require "amigo"
6
6
 
7
7
  # When queues achieve a latency that is too high,
8
8
  # take some action.
9
- # You should start this up at Sidekiq application startup:
9
+ # You should start this up at Web application startup:
10
10
  #
11
- # # sidekiq.rb
12
- # Amigo::Autoscaler.new.start
11
+ # # puma.rb or similar
12
+ # Amigo::Autoscaler.new.start
13
13
  #
14
- # Right now, this is pretty simple- we alert any time
15
- # there is a latency over a threshold.
14
+ # When latency grows beyond +latency_threshold+,
15
+ # a "high latency event" is started.
16
+ # Some action is taken, which is defined by the +handlers+ argument.
17
+ # This includes logging, alerting, and/or autoscaling.
16
18
  #
17
- # In the future, we can:
19
+ # When latency returns to normal (defined by +latency_restored_threshold+),
20
+ # the high latency event finishes.
21
+ # Some additional action is taken, which is defined by the +latency_restored_handlers+ argument.
22
+ # Usually this is logging, and/or returning autoscaling to its original status.
18
23
  #
19
- # 1) actually autoscale rather than just alert
20
- # (this may take the form of a POST to a configurable endpoint),
21
- # 2) become more sophisticated with how we detect latency growth.
24
+ # There are several parameters to control behavior, such as how often polling is done,
25
+ # how often alerting/scaling is done, and more.
22
26
  #
27
+ # As an example autoscaler that includes actual resource scaling,
28
+ # check out +Amigo::Autoscaler::Heroku+.
29
+ # Its ideas can easily be expanded to other platforms.
30
+ #
31
+ # Note that +Autoscaler+ maintains its state over multiple processes;
32
+ # it needs to keep track of high latency events even if the process running the autoscaler
33
+ # (usually a web process) restarts.
23
34
  module Amigo
24
35
  class Autoscaler
25
36
  class InvalidHandler < StandardError; end
@@ -31,18 +42,26 @@ module Amigo
31
42
  # @return [Integer]
32
43
  attr_reader :latency_threshold
33
44
  # What hosts/processes should this run on?
34
- # Look at ENV['DYNO'] and Socket.gethostname.
45
+ # Looks at ENV['DYNO'] and Socket.gethostname for a match.
35
46
  # Default to only run on 'web.1', which is the first Heroku web dyno.
36
47
  # We run on the web, not worker, dyno, so we report backed up queues
37
48
  # in case we, say, turn off all workers (broken web processes
38
49
  # are generally easier to find).
39
50
  # @return [Regexp]
40
51
  attr_reader :hostname_regex
41
- # Methods to call when alerting.
42
- # Valid values are 'log' and 'sentry' (requires Sentry to be required already).
43
- # Anything that responds to +call+ will be invoked with a hash of
44
- # `{queue name => latency in seconds}`.
45
- # @return [Array<String,Proc>]
52
+ # Methods to call when alerting, as strings/symbols or procs.
53
+ # Valid string values are 'log' and 'sentry' (requires Sentry to be required already).
54
+ # Anything that responds to +call+ will be invoked with:
55
+ # - Positional argument which is a +Hash+ of `{queue name => latency in seconds}`
56
+ # - Keyword argument +:depth+: Number of alerts as part of this latency event.
57
+ # For example, the first alert has a depth of 1, and if latency stays high,
58
+ # it'll be 2 on the next call, etc. +depth+ can be used to incrementally provision
59
+ # additional processing capacity, and stop adding capacity at a certain depth
60
+ # to avoid problems with too many workers (like excessive DB load).
61
+ # - Keyword argument +:duration+: Number of seconds since this latency spike started.
62
+ # - Additional undefined keywords. Handlers should accept additional options,
63
+ # like via `**kw` or `opts={}`, for compatibility.
64
+ # @return [Array<String,Symbol,Proc,#call>]
46
65
  attr_reader :handlers
47
66
  # Only alert this often.
48
67
  # For example, with poll_interval of 10 seconds
@@ -50,20 +69,55 @@ module Amigo
50
69
  # we'd alert once and then 210 seconds later.
51
70
  # @return [Integer]
52
71
  attr_reader :alert_interval
72
+ # After an alert happens, what latency should be considered "back to normal" and
73
+ # +latency_restored_handlers+ will be called?
74
+ # In most cases this should be the same as (and defaults to) +latency_threshold+
75
+ # so that we're 'back to normal' once we're below the threshold.
76
+ # It may also commonly be 0, so that the callback is fired when the queue is entirely clear.
77
+ # Note that, if +latency_restored_threshold+ is less than +latency_threshold+,
78
+ # while the latency is between the two, no alerts will fire.
79
+ attr_reader :latency_restored_threshold
80
+ # Methods to call when a latency of +latency_restored_threshold+ is reached
81
+ # (ie, when we get back to normal latency after a high latency event).
82
+ # Valid string values are 'log'.
83
+ # Usually this handler will deprovision capacity procured as part of the alert +handlers+.
84
+ # Anything that responds to +call+ will be invoked with:
85
+ # - Keyword +:depth+, the number of times an alert happened before
86
+ # the latency spike was resolved.
87
+ # - Keyword +:duration+, the number of seconds for the latency spike has been going on.
88
+ # - Additional undefined keywords. Handlers should accept additional options,
89
+ # like via `**kw`, for compatibility.
90
+ # @return [Array<String,Symbol,Proc,#call>]
91
+ attr_reader :latency_restored_handlers
92
+ # Proc/callable called with (level, message, params={}).
93
+ # By default, use +Amigo.log+ (which logs to the Sidekiq logger).
94
+ attr_reader :log
53
95
 
54
96
  def initialize(
55
97
  poll_interval: 20,
56
98
  latency_threshold: 5,
57
99
  hostname_regex: /^web\.1$/,
58
- handlers: ["log"],
59
- alert_interval: 120
100
+ handlers: [:log],
101
+ alert_interval: 120,
102
+ latency_restored_threshold: latency_threshold,
103
+ latency_restored_handlers: [:log],
104
+ log: ->(level, message, params={}) { Amigo.log(nil, level, message, params) }
60
105
  )
61
106
 
107
+ raise ArgumentError, "latency_threshold must be > 0" if
108
+ latency_threshold <= 0
109
+ raise ArgumentError, "latency_restored_threshold must be >= 0" if
110
+ latency_restored_threshold.negative?
111
+ raise ArgumentError, "latency_restored_threshold must be <= latency_threshold" if
112
+ latency_restored_threshold > latency_threshold
62
113
  @poll_interval = poll_interval
63
114
  @latency_threshold = latency_threshold
64
115
  @hostname_regex = hostname_regex
65
- @handlers = handlers
116
+ @handlers = handlers.freeze
66
117
  @alert_interval = alert_interval
118
+ @latency_restored_threshold = latency_restored_threshold
119
+ @latency_restored_handlers = latency_restored_handlers.freeze
120
+ @log = log
67
121
  end
68
122
 
69
123
  def polling_thread
@@ -73,17 +127,44 @@ module Amigo
73
127
  def setup
74
128
  # Store these as strings OR procs, rather than grabbing self.method here.
75
129
  # It gets extremely hard ot test if we capture the method here.
76
- @alert_methods = self.handlers.map do |a|
77
- if a.respond_to?(:call)
78
- a
79
- else
80
- method_name = meth = "alert_#{a.strip}".to_sym
81
- raise InvalidHandler, a.inspect unless self.method(method_name)
82
- meth
83
- end
84
- end
85
- @last_alerted = Time.at(0)
130
+ @alert_methods = self.handlers.map { |a| _handler_to_method("alert_", a) }
131
+ @restored_methods = self.latency_restored_handlers.map { |a| _handler_to_method("alert_restored_", a) }
86
132
  @stop = false
133
+ Sidekiq.redis do |r|
134
+ @last_alerted = Time.at((r.get("#{namespace}/last_alerted") || 0).to_f)
135
+ @depth = (r.get("#{namespace}/depth") || 0).to_i
136
+ @latency_event_started = Time.at((r.get("#{namespace}/latency_event_started") || 0).to_f)
137
+ end
138
+ end
139
+
140
+ private def persist
141
+ Sidekiq.redis do |r|
142
+ r.set("#{namespace}/last_alerted", @last_alerted.to_f.to_s)
143
+ r.set("#{namespace}/depth", @depth.to_s)
144
+ r.set("#{namespace}/latency_event_started", @latency_event_started.to_f.to_s)
145
+ end
146
+ end
147
+
148
+ # Delete all the keys that Autoscaler stores.
149
+ # Can be used in extreme cases where things need to be cleaned up,
150
+ # but should not be normally used.
151
+ def unpersist
152
+ Sidekiq.redis do |r|
153
+ r.del("#{namespace}/last_alerted")
154
+ r.del("#{namespace}/depth")
155
+ r.del("#{namespace}/latency_event_started")
156
+ end
157
+ end
158
+
159
+ protected def namespace
160
+ return "amigo/autoscaler"
161
+ end
162
+
163
+ private def _handler_to_method(prefix, a)
164
+ return a if a.respond_to?(:call)
165
+ method_name = "#{prefix}#{a.to_s.strip}".to_sym
166
+ raise InvalidHandler, a.inspect unless (meth = self.method(method_name))
167
+ return meth
87
168
  end
88
169
 
89
170
  def start
@@ -92,7 +173,7 @@ module Amigo
92
173
  hostname = ENV.fetch("DYNO") { Socket.gethostname }
93
174
  return false unless self.hostname_regex.match?(hostname)
94
175
 
95
- self.log(:info, "async_autoscaler_starting")
176
+ self._log(:info, "async_autoscaler_starting")
96
177
  self.setup
97
178
  @polling_thread = Thread.new do
98
179
  until @stop
@@ -109,21 +190,53 @@ module Amigo
109
190
 
110
191
  def check
111
192
  now = Time.now
112
- skip_check = now < (@last_alerted + self.poll_interval)
193
+ skip_check = now < (@last_alerted + self.alert_interval)
113
194
  if skip_check
114
- self.log(:debug, "async_autoscaler_skip_check")
195
+ self._log(:debug, "async_autoscaler_skip_check")
115
196
  return
116
197
  end
117
- self.log(:info, "async_autoscaler_check")
198
+ self._log(:info, "async_autoscaler_check")
118
199
  high_latency_queues = Sidekiq::Queue.all.
119
200
  map { |q| [q.name, q.latency] }.
120
201
  select { |(_, latency)| latency > self.latency_threshold }.
121
202
  to_h
122
- return if high_latency_queues.empty?
203
+ if high_latency_queues.empty?
204
+ # Whenever we are in a latency event, we have a depth > 0. So a depth of 0 means
205
+ # we're not in a latency event, and still have no latency, so can noop.
206
+ return if @depth.zero?
207
+ # We WERE in a latency event, and now we're not, so report on it.
208
+ @restored_methods.each do |m|
209
+ m.call(depth: @depth, duration: (Time.now - @latency_event_started).to_f)
210
+ end
211
+ # Reset back to 0 depth so we know we're not in a latency event.
212
+ @depth = 0
213
+ @latency_event_started = Time.at(0)
214
+ @last_alerted = now
215
+ self.persist
216
+ return
217
+ end
218
+ if @depth.positive?
219
+ # We have already alerted, so increment the depth and when the latency started.
220
+ @depth += 1
221
+ duration = (Time.now - @latency_event_started).to_f
222
+ else
223
+ # Indicate we are starting a high latency event.
224
+ @depth = 1
225
+ @latency_event_started = Time.now
226
+ duration = 0.0
227
+ end
228
+ # Alert each handler. For legacy reasons, we support handlers that accept
229
+ # ({queues and latencies}) and ({queues and latencies}, {}keywords}).
230
+ kw = {depth: @depth, duration: duration}
123
231
  @alert_methods.each do |m|
124
- m.respond_to?(:call) ? m.call(high_latency_queues) : self.send(m, high_latency_queues)
232
+ if m.respond_to?(:arity) && m.arity == 1
233
+ m.call(high_latency_queues)
234
+ else
235
+ m.call(high_latency_queues, **kw)
236
+ end
125
237
  end
126
238
  @last_alerted = now
239
+ self.persist
127
240
  end
128
241
 
129
242
  def alert_sentry(names_and_latencies)
@@ -134,14 +247,18 @@ module Amigo
134
247
  end
135
248
  end
136
249
 
137
- def alert_log(names_and_latencies)
138
- self.log(:warn, "high_latency_queues", queues: names_and_latencies)
250
+ def alert_log(names_and_latencies, depth:, duration:)
251
+ self._log(:warn, "high_latency_queues", queues: names_and_latencies, depth: depth, duration: duration)
139
252
  end
140
253
 
141
- def alert_test(_names_and_latencies); end
254
+ def alert_test(_names_and_latencies, _opts={}); end
255
+
256
+ def alert_restored_log(depth:, duration:)
257
+ self._log(:info, "high_latency_queues_restored", depth: depth, duration: duration)
258
+ end
142
259
 
143
- protected def log(level, msg, **kw)
144
- Amigo.log(nil, level, msg, kw)
260
+ protected def _log(level, msg, **kw)
261
+ self.log[level, msg, kw]
145
262
  end
146
263
  end
147
264
  end
data/lib/amigo/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Amigo
4
- VERSION = "1.5.0"
4
+ VERSION = "1.6.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sidekiq-amigo
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.0
4
+ version: 1.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lithic Technology
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-03-04 00:00:00.000000000 Z
11
+ date: 2023-04-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sidekiq
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '1'
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: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">"
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: rack
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -86,28 +100,28 @@ dependencies:
86
100
  requirements:
87
101
  - - "~>"
88
102
  - !ruby/object:Gem::Version
89
- version: '1.11'
103
+ version: '1.48'
90
104
  type: :development
91
105
  prerelease: false
92
106
  version_requirements: !ruby/object:Gem::Requirement
93
107
  requirements:
94
108
  - - "~>"
95
109
  - !ruby/object:Gem::Version
96
- version: '1.11'
110
+ version: '1.48'
97
111
  - !ruby/object:Gem::Dependency
98
112
  name: rubocop-performance
99
113
  requirement: !ruby/object:Gem::Requirement
100
114
  requirements:
101
115
  - - "~>"
102
116
  - !ruby/object:Gem::Version
103
- version: '1.10'
117
+ version: '1.16'
104
118
  type: :development
105
119
  prerelease: false
106
120
  version_requirements: !ruby/object:Gem::Requirement
107
121
  requirements:
108
122
  - - "~>"
109
123
  - !ruby/object:Gem::Version
110
- version: '1.10'
124
+ version: '1.16'
111
125
  - !ruby/object:Gem::Dependency
112
126
  name: sentry-ruby
113
127
  requirement: !ruby/object:Gem::Requirement
@@ -136,10 +150,24 @@ dependencies:
136
150
  - - "~>"
137
151
  - !ruby/object:Gem::Version
138
152
  version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: webmock
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">"
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">"
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
139
167
  description: 'sidekiq-amigo provides a pubsub system and other enhancements around
140
168
  Sidekiq.
141
169
 
142
- '
170
+ '
143
171
  email: hello@lithic.tech
144
172
  executables: []
145
173
  extensions: []
@@ -148,6 +176,7 @@ files:
148
176
  - lib/amigo.rb
149
177
  - lib/amigo/audit_logger.rb
150
178
  - lib/amigo/autoscaler.rb
179
+ - lib/amigo/autoscaler/heroku.rb
151
180
  - lib/amigo/deprecated_jobs.rb
152
181
  - lib/amigo/job.rb
153
182
  - lib/amigo/queue_backoff_job.rb
@@ -163,7 +192,7 @@ licenses:
163
192
  - MIT
164
193
  metadata:
165
194
  rubygems_mfa_required: 'true'
166
- post_install_message:
195
+ post_install_message:
167
196
  rdoc_options: []
168
197
  require_paths:
169
198
  - lib
@@ -178,8 +207,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
178
207
  - !ruby/object:Gem::Version
179
208
  version: '0'
180
209
  requirements: []
181
- rubygems_version: 3.1.6
182
- signing_key:
210
+ rubygems_version: 3.3.7
211
+ signing_key:
183
212
  specification_version: 4
184
213
  summary: Pubsub system and other enhancements around Sidekiq.
185
214
  test_files: []