sidekiq-amigo 1.9.0 → 1.11.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: 330ead7f8face652362d632c9c127aa57b298d5587d1da392769b0458d1f8915
4
- data.tar.gz: 7c0b0b9b1658eecfd202ef294580417233421edf4233eef522b747ce9c411f52
3
+ metadata.gz: 5e1aa001f6060b1e13decb95357627151007e8f17066cef3401021103a3cd7a2
4
+ data.tar.gz: 955cb548d0739f5fa51bc0fe887a4e0df5d15c154ee5470e4bdba4883a308af4
5
5
  SHA512:
6
- metadata.gz: 898c24b82de6313c33e50b58a2c4d64ca365918a0aee2b223b452fea9428cc4d265b15975ebc2f7c7e689f547898e65b7c52d3e50edbe9bcdb062f3b25f3f0b6
7
- data.tar.gz: a5ffcb482859fcddd6727a9869bef3ed00c8edb5b1b44f0cde84b31eddb44c6f84fbac6b45d8b8fccf06c712f6792a8ce3e211b4a49083fe8c0bab76474a5d38
6
+ metadata.gz: 9fb081204d9465257ac48df3193925bb547961971fc0db39ea235584e9f4b9acef7ab2f93eaed60c1dcd4d1de759d09daadc5a94ec89473e600eb72b67c4afc7
7
+ data.tar.gz: bea97ecddf27912029035a941e6ee3aff653c0f8ba958e27f21a539f984a987c017311aa21b5f306832bb2240a0f487522e0cd63bf5a87988e5e568ebf503c7b
@@ -4,7 +4,7 @@ require "amigo"
4
4
 
5
5
  module Amigo
6
6
  class AuditLogger
7
- include Sidekiq::Worker
7
+ include Sidekiq::Job
8
8
 
9
9
  def audit_log_level
10
10
  return :info
@@ -35,6 +35,13 @@ module Amigo
35
35
  class Autoscaler
36
36
  class InvalidHandler < StandardError; end
37
37
 
38
+ # Struct representing data serialized to Redis.
39
+ # Useful for diagnostics. Can be retried with +fetch_persisted+.
40
+ # @!attribute last_alerted_at [Time] 0-time if there is no recent alert.
41
+ # @!attribute depth [Integer] 0 if not in a latency event.
42
+ # @!attribute latency_event_started_at [Time] 0-time if not in a latency event.
43
+ Persisted = Struct.new(:last_alerted_at, :depth, :latency_event_started_at)
44
+
38
45
  # How often should Autoscaler check for latency?
39
46
  # @return [Integer]
40
47
  attr_reader :poll_interval
@@ -139,10 +146,19 @@ module Amigo
139
146
  @alert_methods = self.handlers.map { |a| _handler_to_method("alert_", a) }
140
147
  @restored_methods = self.latency_restored_handlers.map { |a| _handler_to_method("alert_restored_", a) }
141
148
  @stop = false
142
- Sidekiq.redis do |r|
143
- @last_alerted = Time.at((r.get("#{namespace}/last_alerted") || 0).to_f)
144
- @depth = (r.get("#{namespace}/depth") || 0).to_i
145
- @latency_event_started = Time.at((r.get("#{namespace}/latency_event_started") || 0).to_f)
149
+ persisted = self.fetch_persisted
150
+ @last_alerted = persisted.last_alerted_at
151
+ @depth = persisted.depth
152
+ @latency_event_started = persisted.latency_event_started_at
153
+ end
154
+
155
+ def fetch_persisted
156
+ return Sidekiq.redis do |r|
157
+ Persisted.new(
158
+ Time.at((r.get("#{namespace}/last_alerted") || 0).to_f),
159
+ (r.get("#{namespace}/depth") || 0).to_i,
160
+ Time.at((r.get("#{namespace}/latency_event_started") || 0).to_f),
161
+ )
146
162
  end
147
163
  end
148
164
 
data/lib/amigo/job.rb CHANGED
@@ -7,7 +7,7 @@ require "amigo"
7
7
  module Amigo
8
8
  module Job
9
9
  def self.extended(cls)
10
- cls.include(Sidekiq::Worker)
10
+ cls.include(Sidekiq::Job)
11
11
  cls.extend(ClassMethods)
12
12
  cls.pattern = ""
13
13
  cls.include(InstanceMethods)
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Amigo
4
+ # Helper class to detect when the Redis server is under memory pressure.
5
+ # In these cases, we want to disable queue backoff behavior.
6
+ # There is a significant risk that the backoff behavior will take jobs from the queue,
7
+ # and immediately try and reschedule them. If that happens in an OOM condition,
8
+ # the re-push will fail and the job can be lost.
9
+ #
10
+ # Additionally, the backoff behavior causes delays that slow down the clearing of the queue.
11
+ #
12
+ # In these high-memory-utilization conditions, it makes more sense to disable the backoff logic
13
+ # and just brute force to try to get through the queue.
14
+ class MemoryPressure
15
+ # Percentage at which the server is considered under memory pressure.
16
+ DEFAULT_THRESHOLD = 90
17
+
18
+ # Default seconds a memory check is good for. See +check_ttl+.
19
+ DEFAULT_CHECK_TTL = 120
20
+
21
+ class << self
22
+ # Return the singleton instance, creating a cached value if needed.
23
+ def instance
24
+ return @instance ||= self.new
25
+ end
26
+
27
+ # Set the instance, or use nil to reset.
28
+ attr_writer :instance
29
+ end
30
+
31
+ # When did we last check for pressure?
32
+ attr_reader :last_checked_at
33
+
34
+ # What was the result of the last check?
35
+ # true is under pressure, false if not.
36
+ attr_reader :last_check_result
37
+
38
+ # See +DEFAULT_CHECK_TTL+.
39
+ attr_reader :check_ttl
40
+
41
+ # See +DEFAULT_THRESHOLD+.
42
+ attr_reader :threshold
43
+
44
+ def initialize(check_ttl: DEFAULT_CHECK_TTL, threshold: DEFAULT_THRESHOLD)
45
+ @last_checked_at = nil
46
+ @check_ttl = check_ttl
47
+ @threshold = threshold
48
+ @last_check_result = nil
49
+ end
50
+
51
+ # Return true if the server is under memory pressure.
52
+ # When this is the case, we want to disable backoff,
53
+ # since it will delay working through the queue,
54
+ # and can also result in a higher likelihood of lost jobs,
55
+ # since returning them back to the queue will fail.
56
+ def under_pressure?
57
+ return @last_check_result unless self.needs_check?
58
+ @last_check_result = self.calculate_under_pressure
59
+ @last_checked_at = Time.now
60
+ return @last_check_result
61
+ end
62
+
63
+ private def needs_check?
64
+ return true if @last_checked_at.nil?
65
+ return (@last_checked_at + @check_ttl) < Time.now
66
+ end
67
+
68
+ private def calculate_under_pressure
69
+ meminfo = self.get_memory_info
70
+ used_bytes = meminfo.fetch("used_memory", "0").to_f
71
+ max_bytes = meminfo.fetch("maxmemory", "0").to_f
72
+ return false if used_bytes.zero? || max_bytes.zero?
73
+ percentage = (used_bytes / max_bytes) * 100
74
+ return percentage > self.threshold
75
+ end
76
+
77
+ def get_memory_info
78
+ s = self.get_memory_info_string
79
+ return self.parse_memory_string(s)
80
+ end
81
+
82
+ protected def get_memory_info_string
83
+ s = Sidekiq.redis do |c|
84
+ c.call("INFO", "MEMORY")
85
+ end
86
+ return s
87
+ end
88
+
89
+ protected def parse_memory_string(s)
90
+ # See bottom of https://redis.io/docs/latest/commands/info/ for format.
91
+ pairs = s.split("\r\n").reject { |line| line.start_with?("#") }.map { |pair| pair.split(":", 2) }
92
+ h = pairs.to_h
93
+ return h
94
+ end
95
+ end
96
+ end
@@ -3,6 +3,8 @@
3
3
  require "sidekiq"
4
4
  require "sidekiq/api"
5
5
 
6
+ require "amigo/memory_pressure"
7
+
6
8
  # Queue backoff jobs are used for jobs that should not saturate workers,
7
9
  # such that jobs on dependent queues end up not running for a while.
8
10
  #
@@ -45,6 +47,11 @@ require "sidekiq/api"
45
47
  # This is a fast call (it just gets the last item), but it's not free,
46
48
  # so users should be aware of it.
47
49
  #
50
+ # == High Memory Utilization
51
+ #
52
+ # Queue backoff behavior is automatically disabled under high memory utilization,
53
+ # as per +Amigo::MemoryPressure+.
54
+ #
48
55
  module Amigo
49
56
  module QueueBackoffJob
50
57
  def self.included(cls)
@@ -126,6 +133,7 @@ module Amigo
126
133
  module PrependedMethods
127
134
  def perform(*args)
128
135
  return super unless ::Amigo::QueueBackoffJob.enabled?
136
+ return super if ::Amigo::MemoryPressure.instance.under_pressure?
129
137
  # rubocop:disable Style/GuardClause, Lint/NonLocalExitFromIterator
130
138
  dependent_queues.each do |qname|
131
139
  latency = Amigo::QueueBackoffJob.check_latency(qname)
data/lib/amigo/retry.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  require "sidekiq"
4
4
  require "sidekiq/api"
5
5
 
6
- # Middleware so Sidekiq workers can use a custom retry logic.
6
+ # Middleware so Sidekiq jobs can use a custom retry logic.
7
7
  # See +Amigo::Retry::Retry+, +Amigo::Retry::Die+,
8
8
  # and +Amigo::Retry::OrDie+ for more details
9
9
  # on how these should be used.
@@ -83,6 +83,8 @@ module Amigo
83
83
  end
84
84
 
85
85
  class ServerMiddleware
86
+ include Sidekiq::ServerMiddleware
87
+
86
88
  def call(worker, job, _queue)
87
89
  yield
88
90
  rescue Amigo::Retry::Retry => e
@@ -120,14 +122,14 @@ module Amigo
120
122
  end
121
123
  end
122
124
 
123
- def amigo_retry_in(worker_class, item, interval)
125
+ def amigo_retry_in(job_class, item, interval)
124
126
  # pulled from perform_in
125
127
  int = interval.to_f
126
128
  now = Time.now.to_f
127
129
  ts = (int < 1_000_000_000 ? now + int : int)
128
130
  item["at"] = ts if ts > now
129
131
  item["retry_count"] = item.fetch("retry_count", 0) + 1
130
- worker_class.client_push(item)
132
+ job_class.client_push(item)
131
133
  end
132
134
  end
133
135
  end
data/lib/amigo/router.rb CHANGED
@@ -6,7 +6,7 @@ require "amigo"
6
6
 
7
7
  module Amigo
8
8
  class Router
9
- include Sidekiq::Worker
9
+ include Sidekiq::Job
10
10
 
11
11
  def perform(event_json)
12
12
  event_name = event_json["name"]
@@ -8,7 +8,7 @@ require "amigo"
8
8
  module Amigo
9
9
  module ScheduledJob
10
10
  def self.extended(cls)
11
- cls.include(Sidekiq::Worker)
11
+ cls.include(Sidekiq::Job)
12
12
  cls.sidekiq_options(retry: false)
13
13
  cls.extend(ClassMethods)
14
14
  cls.splay_duration = 30
@@ -2,9 +2,8 @@
2
2
 
3
3
  require "sidekiq"
4
4
 
5
- module Amigo
6
- # This is a placeholder until it's migrated to Amigo proper
7
- end
5
+ require "amigo"
6
+ require "amigo/memory_pressure"
8
7
 
9
8
  # Semaphore backoff jobs can reschedule themselves to happen at a later time
10
9
  # if there is too high a contention on a semaphore.
@@ -34,7 +33,7 @@ end
34
33
  # - `semaphore_expiry` should return the TTL of the semaphore key.
35
34
  # Defaults to 30 seconds. See below for key expiry and negative semaphore value details.
36
35
  # - `before_perform` is called before calling the `perform` method.
37
- # This is required so that implementers can set worker state, based on job arguments,
36
+ # This is required so that implementers can set job state, based on job arguments,
38
37
  # that can be used for calculating the semaphore key.
39
38
  #
40
39
  # Note that we give the semaphore key an expiry. This is to avoid situation where
@@ -42,12 +41,17 @@ end
42
41
  # have fewer than the expected number of jobs running.
43
42
  #
44
43
  # This does mean that, when a job runs longer than the semaphore expiry,
45
- # another worker can be started, which would increment the counter back to 1.
44
+ # another job can be started, which would increment the counter back to 1.
46
45
  # When the original job ends, the counter would be 0; then when the new job ends,
47
46
  # the counter would be -1. To avoid negative counters (which create the same issue
48
47
  # around missing decrements), if we ever detect a negative 'jobs running',
49
48
  # we warn and remove the key entirely.
50
49
  #
50
+ # == High Memory Utilization
51
+ #
52
+ # Queue backoff behavior is automatically disabled under high memory utilization,
53
+ # as per +Amigo::MemoryPressure+.
54
+ #
51
55
  module Amigo
52
56
  module SemaphoreBackoffJob
53
57
  def self.included(cls)
@@ -74,11 +78,11 @@ module Amigo
74
78
 
75
79
  module InstanceMethods
76
80
  def semaphore_key
77
- raise NotImplementedError, "must be implemented on worker"
81
+ raise NotImplementedError, "must be implemented on job"
78
82
  end
79
83
 
80
84
  def semaphore_size
81
- raise NotImplementedError, "must be implemented on worker"
85
+ raise NotImplementedError, "must be implemented on job"
82
86
  end
83
87
 
84
88
  def semaphore_backoff
@@ -94,6 +98,7 @@ module Amigo
94
98
  def perform(*args)
95
99
  self.before_perform(*args) if self.respond_to?(:before_perform)
96
100
  return super unless ::Amigo::SemaphoreBackoffJob.enabled?
101
+ return super if ::Amigo::MemoryPressure.instance.under_pressure?
97
102
  key = self.semaphore_key
98
103
  size = self.semaphore_size
99
104
  # Create a simple counter for the semaphore key.
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "amigo"
4
- require "sidekiq/worker"
5
4
 
6
5
  module Amigo
7
6
  module SpecHelpers
@@ -248,7 +247,7 @@ module Amigo
248
247
  return PerformAsyncJobMatcher.new(job)
249
248
  end
250
249
 
251
- # Like a Sidekiq worker's perform_inline,
250
+ # Like a Sidekiq job's perform_inline,
252
251
  # but allows an arbitrary item to be used, rather than just the
253
252
  # given class and args. For example, when testing,
254
253
  # you may need to assume something like 'retry_count' is in the job payload,
@@ -256,18 +255,18 @@ module Amigo
256
255
  # This allows those arbitrary job payload fields
257
256
  # to be included when the job is run.
258
257
  module_function def sidekiq_perform_inline(klass, args, item=nil)
259
- Sidekiq::Worker::Setter.override_item = item
258
+ Sidekiq::Job::Setter.override_item = item
260
259
  begin
261
260
  klass.perform_inline(*args)
262
261
  ensure
263
- Sidekiq::Worker::Setter.override_item = nil
262
+ Sidekiq::Job::Setter.override_item = nil
264
263
  end
265
264
  end
266
265
 
267
266
  module_function def drain_sidekiq_jobs(q)
268
267
  all_sidekiq_jobs(q).each do |job|
269
268
  klass = job.item.fetch("class")
270
- klass = Sidekiq::Testing.constantize(klass) if klass.is_a?(String)
269
+ klass = Object.const_get(klass) if klass.is_a?(String)
271
270
  sidekiq_perform_inline(klass, job.item["args"], job.item)
272
271
  job.delete
273
272
  end
@@ -282,6 +281,8 @@ module Amigo
282
281
  # Use this middleware to pass an arbitrary callback evaluated before a job runs.
283
282
  # Make sure to call +reset+ after the test.
284
283
  class ServerCallbackMiddleware
284
+ include Sidekiq::ServerMiddleware
285
+
285
286
  class << self
286
287
  attr_accessor :callback
287
288
  end
@@ -304,7 +305,7 @@ module Amigo
304
305
  end
305
306
 
306
307
  module ::Sidekiq
307
- module Worker
308
+ module Job
308
309
  class Setter
309
310
  class << self
310
311
  attr_accessor :override_item
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.9.0"
4
+ VERSION = "1.11.0"
5
5
  end
data/lib/amigo.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "redis"
4
3
  require "sidekiq"
5
4
  require "sidekiq-cron"
6
5
 
@@ -61,18 +60,18 @@ require "sidekiq-cron"
61
60
  # to control the matching rules more closely than File.fnmatch can provide.
62
61
  #
63
62
  # Jobs must implement a `_perform` method, which takes a Amigo::Event.
64
- # Note that normal Sidekiq workers use a 'perform' method that takes a variable number of arguments;
63
+ # Note that normal Sidekiq jobs use a 'perform' method that takes a variable number of arguments;
65
64
  # the base Async::Job class has this method and delegates its business logic to the subclass _perform method.
66
65
  #
67
66
  # Routing
68
67
  #
69
- # There are two special workers that are important for the overall functioning of the system
70
- # (and do not inherit from Job but rather than Sidekiq::Worker so they are not classified and treated as 'Jobs').
68
+ # There are two special jobs that are important for the overall functioning of the system
69
+ # (and do not inherit from Job but rather than Sidekiq::Job so they are not classified and treated as 'Jobs').
71
70
  #
72
71
  # The first is the AuditLogger, which is a basic job that logs all async events.
73
72
  # This acts as a useful change log for the state of the database.
74
73
  #
75
- # The second special worker is the Router, which calls `perform` on the event Jobs
74
+ # The second special job is the Router, which calls `perform` on the event Jobs
76
75
  # that match the routing information, as explained in Jobs.
77
76
  # It does this by filtering through all event-based jobs and performing the ones with a route match.
78
77
  #
metadata CHANGED
@@ -1,43 +1,42 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sidekiq-amigo
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.0
4
+ version: 1.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lithic Technology
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-02-03 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: sidekiq
15
14
  requirement: !ruby/object:Gem::Requirement
16
15
  requirements:
17
- - - "~>"
16
+ - - ">="
18
17
  - !ruby/object:Gem::Version
19
- version: '6'
18
+ version: '7'
20
19
  type: :runtime
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
- - - "~>"
23
+ - - ">="
25
24
  - !ruby/object:Gem::Version
26
- version: '6'
25
+ version: '7'
27
26
  - !ruby/object:Gem::Dependency
28
27
  name: sidekiq-cron
29
28
  requirement: !ruby/object:Gem::Requirement
30
29
  requirements:
31
30
  - - "~>"
32
31
  - !ruby/object:Gem::Version
33
- version: '1'
32
+ version: '2'
34
33
  type: :runtime
35
34
  prerelease: false
36
35
  version_requirements: !ruby/object:Gem::Requirement
37
36
  requirements:
38
37
  - - "~>"
39
38
  - !ruby/object:Gem::Version
40
- version: '1'
39
+ version: '2'
41
40
  - !ruby/object:Gem::Dependency
42
41
  name: platform-api
43
42
  requirement: !ruby/object:Gem::Requirement
@@ -58,14 +57,14 @@ dependencies:
58
57
  requirements:
59
58
  - - "~>"
60
59
  - !ruby/object:Gem::Version
61
- version: '2.2'
60
+ version: '3.1'
62
61
  type: :development
63
62
  prerelease: false
64
63
  version_requirements: !ruby/object:Gem::Requirement
65
64
  requirements:
66
65
  - - "~>"
67
66
  - !ruby/object:Gem::Version
68
- version: '2.2'
67
+ version: '3.1'
69
68
  - !ruby/object:Gem::Dependency
70
69
  name: rspec
71
70
  requirement: !ruby/object:Gem::Requirement
@@ -179,6 +178,7 @@ files:
179
178
  - lib/amigo/autoscaler/heroku.rb
180
179
  - lib/amigo/deprecated_jobs.rb
181
180
  - lib/amigo/job.rb
181
+ - lib/amigo/memory_pressure.rb
182
182
  - lib/amigo/queue_backoff_job.rb
183
183
  - lib/amigo/rate_limited_error_handler.rb
184
184
  - lib/amigo/retry.rb
@@ -192,7 +192,6 @@ licenses:
192
192
  - MIT
193
193
  metadata:
194
194
  rubygems_mfa_required: 'true'
195
- post_install_message:
196
195
  rdoc_options: []
197
196
  require_paths:
198
197
  - lib
@@ -200,15 +199,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
200
199
  requirements:
201
200
  - - ">="
202
201
  - !ruby/object:Gem::Version
203
- version: 3.0.0
202
+ version: 3.2.0
204
203
  required_rubygems_version: !ruby/object:Gem::Requirement
205
204
  requirements:
206
205
  - - ">="
207
206
  - !ruby/object:Gem::Version
208
207
  version: '0'
209
208
  requirements: []
210
- rubygems_version: 3.3.7
211
- signing_key:
209
+ rubygems_version: 3.6.7
212
210
  specification_version: 4
213
211
  summary: Pubsub system and other enhancements around Sidekiq.
214
212
  test_files: []