sidekiq-queue-throttled 1.0.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.
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module QueueThrottled
5
+ class Middleware
6
+ def initialize
7
+ @queue_limiters = Concurrent::Map.new
8
+ @job_throttlers = Concurrent::Map.new
9
+ end
10
+
11
+ def call(worker, job, queue)
12
+ queue_name = job['queue'] || queue
13
+ job_class = worker.class.name
14
+
15
+ # Check queue-level limits
16
+ queue_limiter = get_queue_limiter(queue_name)
17
+ if queue_limiter
18
+ lock_id = queue_limiter.acquire_lock
19
+ unless lock_id
20
+ Sidekiq::QueueThrottled.logger.info "Queue limit reached for #{queue_name}, rescheduling job"
21
+ reschedule_job(job, queue_name)
22
+ return nil
23
+ end
24
+ end
25
+
26
+ # Check job-level throttling
27
+ job_throttler = get_job_throttler(job_class)
28
+ if job_throttler && !job_throttler.acquire_slot(job['args'])
29
+ Sidekiq::QueueThrottled.logger.info "Job throttling limit reached for #{job_class}, rescheduling job"
30
+ reschedule_job(job, queue_name)
31
+ return nil
32
+ end
33
+
34
+ # Process the job
35
+ begin
36
+ yield
37
+ ensure
38
+ # Release locks
39
+ queue_limiter&.release_lock(lock_id)
40
+ job_throttler&.release_slot(job['args'])
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def get_queue_limiter(queue_name)
47
+ limit = Sidekiq::QueueThrottled.configuration.queue_limit(queue_name)
48
+ return nil unless limit
49
+
50
+ @queue_limiters.compute_if_absent(queue_name) do
51
+ QueueLimiter.new(queue_name, limit)
52
+ end
53
+ end
54
+
55
+ def get_job_throttler(job_class)
56
+ throttle_config = get_throttle_config(job_class)
57
+ return nil unless throttle_config
58
+
59
+ @job_throttlers.compute_if_absent(job_class) do
60
+ JobThrottler.new(job_class, throttle_config)
61
+ end
62
+ end
63
+
64
+ def get_throttle_config(job_class)
65
+ # Handle string class names
66
+ if job_class.is_a?(String)
67
+ begin
68
+ klass = Object.const_get(job_class)
69
+ return klass.sidekiq_throttle_config if klass.respond_to?(:sidekiq_throttle_config)
70
+ rescue NameError
71
+ # For test classes that don't have proper constant names
72
+ return nil
73
+ end
74
+ elsif job_class.respond_to?(:sidekiq_throttle_config)
75
+ # Handle actual class objects
76
+ return job_class.sidekiq_throttle_config
77
+ end
78
+ nil
79
+ end
80
+
81
+ def reschedule_job(job, queue_name)
82
+ delay = Sidekiq::QueueThrottled.configuration.retry_delay
83
+ job['at'] = Time.now.to_f + delay
84
+ job['queue'] = queue_name
85
+
86
+ Sidekiq.redis do |conn|
87
+ conn.zadd('schedule', job['at'], job.to_json)
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module QueueThrottled
5
+ class QueueLimiter
6
+ attr_reader :queue_name, :limit, :redis
7
+
8
+ def initialize(queue_name, limit, redis = nil)
9
+ @queue_name = queue_name.to_s
10
+ @limit = limit.to_i
11
+ @redis = redis || Sidekiq::QueueThrottled.redis
12
+ @lock_key = "#{Sidekiq::QueueThrottled.configuration.redis_key_prefix}:queue:#{@queue_name}:lock"
13
+ @counter_key = "#{Sidekiq::QueueThrottled.configuration.redis_key_prefix}:queue:#{@queue_name}:counter"
14
+ @mutex = Concurrent::ReentrantReadWriteLock.new
15
+ end
16
+
17
+ def acquire_lock(worker_id = nil)
18
+ worker_id ||= SecureRandom.uuid
19
+ lock_id = "#{worker_id}:#{Time.now.to_f}"
20
+
21
+ @mutex.with_write_lock do
22
+ current_count = get_current_count
23
+ puts "DEBUG: QueueLimiter - current_count: #{current_count}, limit: #{@limit}"
24
+ return false if current_count >= @limit
25
+
26
+ # Increment the counter first
27
+ increment_counter
28
+ puts "DEBUG: QueueLimiter - acquired lock: #{lock_id}"
29
+ return lock_id
30
+ end
31
+
32
+ false
33
+ end
34
+
35
+ def release_lock(lock_id)
36
+ return false unless lock_id
37
+
38
+ @mutex.with_write_lock do
39
+ # For time-based limiting, we don't immediately decrement the counter
40
+ # The counter will expire naturally after the TTL period
41
+ # This prevents immediate reuse of slots
42
+ true
43
+ end
44
+ rescue StandardError => e
45
+ Sidekiq::QueueThrottled.logger.error "Failed to release lock #{lock_id} for queue #{@queue_name}: #{e.message}"
46
+ false
47
+ end
48
+
49
+ def current_count
50
+ @mutex.with_read_lock do
51
+ get_current_count
52
+ end
53
+ end
54
+
55
+ def available_slots
56
+ [0, @limit - current_count].max
57
+ end
58
+
59
+ def reset!
60
+ @mutex.with_write_lock do
61
+ @redis.del(@counter_key)
62
+ # Clear all locks for this queue
63
+ pattern = "#{Sidekiq::QueueThrottled.configuration.redis_key_prefix}:queue:#{@queue_name}:lock:*"
64
+ keys = @redis.keys(pattern)
65
+ @redis.del(*keys) unless keys.empty?
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ def get_current_count
72
+ count = @redis.get(@counter_key)
73
+ result = count ? count.to_i : 0
74
+ puts "DEBUG: get_current_count - key: #{@counter_key}, count: #{result}"
75
+ result
76
+ end
77
+
78
+ def increment_counter
79
+ puts "DEBUG: increment_counter - key: #{@counter_key}"
80
+ @redis.multi do |multi|
81
+ multi.incr(@counter_key)
82
+ multi.expire(@counter_key, Sidekiq::QueueThrottled.configuration.throttle_ttl)
83
+ end
84
+ puts "DEBUG: increment_counter - after increment, count: #{get_current_count}"
85
+ end
86
+
87
+ def decrement_counter
88
+ @redis.multi do |multi|
89
+ multi.decr(@counter_key)
90
+ multi.expire(@counter_key, Sidekiq::QueueThrottled.configuration.throttle_ttl)
91
+ end
92
+ # Ensure counter doesn't go below 0
93
+ current = get_current_count
94
+ if current.negative?
95
+ @redis.set(@counter_key, 0)
96
+ end
97
+ end
98
+
99
+ def acquire_redis_lock(lock_id)
100
+ lock_key = "#{@lock_key}:#{lock_id}"
101
+ @redis.set(lock_key, '1', nx: true, ex: Sidekiq::QueueThrottled.configuration.lock_ttl)
102
+ end
103
+
104
+ def release_redis_lock(lock_id)
105
+ lock_key = "#{@lock_key}:#{lock_id}"
106
+ @redis.del(lock_key).positive?
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module QueueThrottled
5
+ VERSION = '1.0.0'
6
+ end
7
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sidekiq'
4
+ require 'redis'
5
+ require 'concurrent'
6
+ require 'json'
7
+ require 'logger'
8
+
9
+ require_relative 'queue_throttled/version'
10
+ require_relative 'queue_throttled/configuration'
11
+ require_relative 'queue_throttled/queue_limiter'
12
+ require_relative 'queue_throttled/job_throttler'
13
+ require_relative 'queue_throttled/middleware'
14
+ require_relative 'queue_throttled/job'
15
+
16
+ module Sidekiq
17
+ module QueueThrottled
18
+ class << self
19
+ def configure
20
+ yield configuration
21
+ end
22
+
23
+ def configuration
24
+ @configuration ||= Configuration.new
25
+ end
26
+
27
+ attr_writer :configuration, :logger, :redis
28
+
29
+ def logger
30
+ @logger ||= begin
31
+ logger = Logger.new($stdout)
32
+ logger.level = Logger::INFO
33
+ logger
34
+ end
35
+ end
36
+
37
+ def redis
38
+ @redis ||= Sidekiq.redis { |conn| conn }
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ # Auto-load the middleware when the gem is required
45
+ Sidekiq.configure_server do |config|
46
+ config.server_middleware do |chain|
47
+ chain.add Sidekiq::QueueThrottled::Middleware
48
+ end
49
+ end
data/spec/examples.txt ADDED
@@ -0,0 +1,110 @@
1
+ example_id | status | run_time |
2
+ ------------------------------------------------------------- | ------ | --------------- |
3
+ ./spec/sidekiq/queue_throttled/configuration_spec.rb[1:1:1] | passed | 0.0001 seconds |
4
+ ./spec/sidekiq/queue_throttled/configuration_spec.rb[1:1:2] | passed | 0.0001 seconds |
5
+ ./spec/sidekiq/queue_throttled/configuration_spec.rb[1:1:3] | passed | 0.00009 seconds |
6
+ ./spec/sidekiq/queue_throttled/configuration_spec.rb[1:2:1] | passed | 0.0001 seconds |
7
+ ./spec/sidekiq/queue_throttled/configuration_spec.rb[1:2:2] | passed | 0.0001 seconds |
8
+ ./spec/sidekiq/queue_throttled/configuration_spec.rb[1:2:3] | passed | 0.0001 seconds |
9
+ ./spec/sidekiq/queue_throttled/configuration_spec.rb[1:3:1] | passed | 0.00019 seconds |
10
+ ./spec/sidekiq/queue_throttled/configuration_spec.rb[1:3:2] | passed | 0.0002 seconds |
11
+ ./spec/sidekiq/queue_throttled/configuration_spec.rb[1:3:3] | passed | 0.00354 seconds |
12
+ ./spec/sidekiq/queue_throttled/configuration_spec.rb[1:4:1] | passed | 0.00031 seconds |
13
+ ./spec/sidekiq/queue_throttled/configuration_spec.rb[1:4:2] | passed | 0.00437 seconds |
14
+ ./spec/sidekiq/queue_throttled/configuration_spec.rb[1:4:3] | passed | 0.01693 seconds |
15
+ ./spec/sidekiq/queue_throttled/configuration_spec.rb[1:5:1] | passed | 0.0001 seconds |
16
+ ./spec/sidekiq/queue_throttled/configuration_spec.rb[1:5:2] | passed | 0.0001 seconds |
17
+ ./spec/sidekiq/queue_throttled/configuration_spec.rb[1:5:3] | passed | 0.0001 seconds |
18
+ ./spec/sidekiq/queue_throttled/configuration_spec.rb[1:5:4] | passed | 0.00011 seconds |
19
+ ./spec/sidekiq/queue_throttled/configuration_spec.rb[1:6:1] | passed | 0.0001 seconds |
20
+ ./spec/sidekiq/queue_throttled/configuration_spec.rb[1:6:2] | passed | 0.00009 seconds |
21
+ ./spec/sidekiq/queue_throttled/configuration_spec.rb[1:6:3] | passed | 0.0001 seconds |
22
+ ./spec/sidekiq/queue_throttled/configuration_spec.rb[1:6:4] | passed | 0.00009 seconds |
23
+ ./spec/sidekiq/queue_throttled/job_spec.rb[1:1:1] | passed | 0.00014 seconds |
24
+ ./spec/sidekiq/queue_throttled/job_spec.rb[1:1:2] | passed | 0.00013 seconds |
25
+ ./spec/sidekiq/queue_throttled/job_spec.rb[1:2:1] | passed | 0.00015 seconds |
26
+ ./spec/sidekiq/queue_throttled/job_spec.rb[1:2:2] | passed | 0.00019 seconds |
27
+ ./spec/sidekiq/queue_throttled/job_spec.rb[1:2:3] | passed | 0.00012 seconds |
28
+ ./spec/sidekiq/queue_throttled/job_spec.rb[1:2:4] | passed | 0.00015 seconds |
29
+ ./spec/sidekiq/queue_throttled/job_spec.rb[1:2:5] | passed | 0.00017 seconds |
30
+ ./spec/sidekiq/queue_throttled/job_spec.rb[1:2:6] | passed | 0.00015 seconds |
31
+ ./spec/sidekiq/queue_throttled/job_spec.rb[1:2:7] | passed | 0.00015 seconds |
32
+ ./spec/sidekiq/queue_throttled/job_spec.rb[1:2:8] | passed | 0.00016 seconds |
33
+ ./spec/sidekiq/queue_throttled/job_spec.rb[1:2:9] | passed | 0.00016 seconds |
34
+ ./spec/sidekiq/queue_throttled/job_spec.rb[1:2:10] | passed | 0.00016 seconds |
35
+ ./spec/sidekiq/queue_throttled/job_spec.rb[1:2:11] | passed | 0.00015 seconds |
36
+ ./spec/sidekiq/queue_throttled/job_spec.rb[1:2:12] | passed | 0.00016 seconds |
37
+ ./spec/sidekiq/queue_throttled/job_spec.rb[1:2:13] | passed | 0.00016 seconds |
38
+ ./spec/sidekiq/queue_throttled/job_spec.rb[1:3:1] | passed | 0.00072 seconds |
39
+ ./spec/sidekiq/queue_throttled/job_spec.rb[1:3:2] | passed | 0.00139 seconds |
40
+ ./spec/sidekiq/queue_throttled/job_throttler_spec.rb[1:1:1] | passed | 0.00024 seconds |
41
+ ./spec/sidekiq/queue_throttled/job_throttler_spec.rb[1:1:2] | passed | 0.00018 seconds |
42
+ ./spec/sidekiq/queue_throttled/job_throttler_spec.rb[1:2:1:1] | passed | 0.00011 seconds |
43
+ ./spec/sidekiq/queue_throttled/job_throttler_spec.rb[1:2:2:1] | passed | 0.00022 seconds |
44
+ ./spec/sidekiq/queue_throttled/job_throttler_spec.rb[1:2:2:2] | passed | 0.00065 seconds |
45
+ ./spec/sidekiq/queue_throttled/job_throttler_spec.rb[1:2:2:3] | passed | 0.00053 seconds |
46
+ ./spec/sidekiq/queue_throttled/job_throttler_spec.rb[1:2:3:1] | passed | 0.00015 seconds |
47
+ ./spec/sidekiq/queue_throttled/job_throttler_spec.rb[1:2:3:2] | passed | 0.00047 seconds |
48
+ ./spec/sidekiq/queue_throttled/job_throttler_spec.rb[1:2:3:3] | passed | 0.00072 seconds |
49
+ ./spec/sidekiq/queue_throttled/job_throttler_spec.rb[1:2:4:1] | passed | 0.0003 seconds |
50
+ ./spec/sidekiq/queue_throttled/job_throttler_spec.rb[1:3:1:1] | passed | 0.0001 seconds |
51
+ ./spec/sidekiq/queue_throttled/job_throttler_spec.rb[1:3:2:1] | passed | 0.00037 seconds |
52
+ ./spec/sidekiq/queue_throttled/job_throttler_spec.rb[1:3:2:2] | passed | 0.00066 seconds |
53
+ ./spec/sidekiq/queue_throttled/job_throttler_spec.rb[1:3:2:3] | passed | 0.00079 seconds |
54
+ ./spec/sidekiq/queue_throttled/job_throttler_spec.rb[1:3:3:1] | passed | 0.00022 seconds |
55
+ ./spec/sidekiq/queue_throttled/job_throttler_spec.rb[1:3:3:2] | passed | 0.00043 seconds |
56
+ ./spec/sidekiq/queue_throttled/job_throttler_spec.rb[1:4:1:1] | passed | 0.0045 seconds |
57
+ ./spec/sidekiq/queue_throttled/job_throttler_spec.rb[1:4:2:1] | passed | 0.00214 seconds |
58
+ ./spec/sidekiq/queue_throttled/job_throttler_spec.rb[1:4:2:2] | passed | 0.00612 seconds |
59
+ ./spec/sidekiq/queue_throttled/job_throttler_spec.rb[1:4:3:1] | passed | 0.00015 seconds |
60
+ ./spec/sidekiq/queue_throttled/job_throttler_spec.rb[1:5:1:1] | passed | 0.00066 seconds |
61
+ ./spec/sidekiq/queue_throttled/job_throttler_spec.rb[1:5:2:1] | passed | 0.00079 seconds |
62
+ ./spec/sidekiq/queue_throttled/job_throttler_spec.rb[1:5:2:2] | passed | 0.00024 seconds |
63
+ ./spec/sidekiq/queue_throttled/job_throttler_spec.rb[1:5:3:1] | passed | 0.00054 seconds |
64
+ ./spec/sidekiq/queue_throttled/job_throttler_spec.rb[1:5:4:1] | passed | 0.00068 seconds |
65
+ ./spec/sidekiq/queue_throttled/job_throttler_spec.rb[1:6:1] | passed | 0.00155 seconds |
66
+ ./spec/sidekiq/queue_throttled/job_throttler_spec.rb[1:6:2] | passed | 0.00058 seconds |
67
+ ./spec/sidekiq/queue_throttled/job_throttler_spec.rb[1:6:3] | passed | 0.0007 seconds |
68
+ ./spec/sidekiq/queue_throttled/job_throttler_spec.rb[1:7:1] | passed | 0.0013 seconds |
69
+ ./spec/sidekiq/queue_throttled/middleware_spec.rb[1:1:1:1] | passed | 0.00033 seconds |
70
+ ./spec/sidekiq/queue_throttled/middleware_spec.rb[1:1:2:1] | passed | 0.00045 seconds |
71
+ ./spec/sidekiq/queue_throttled/middleware_spec.rb[1:1:2:2] | passed | 0.00296 seconds |
72
+ ./spec/sidekiq/queue_throttled/middleware_spec.rb[1:1:3:1] | passed | 0.00073 seconds |
73
+ ./spec/sidekiq/queue_throttled/middleware_spec.rb[1:1:3:2] | passed | 0.00098 seconds |
74
+ ./spec/sidekiq/queue_throttled/middleware_spec.rb[1:1:3:3] | passed | 0.00104 seconds |
75
+ ./spec/sidekiq/queue_throttled/middleware_spec.rb[1:1:4:1] | passed | 0.00105 seconds |
76
+ ./spec/sidekiq/queue_throttled/middleware_spec.rb[1:1:5:1] | passed | 0.00068 seconds |
77
+ ./spec/sidekiq/queue_throttled/middleware_spec.rb[1:1:5:2] | passed | 0.00223 seconds |
78
+ ./spec/sidekiq/queue_throttled/middleware_spec.rb[1:2:1] | passed | 0.00009 seconds |
79
+ ./spec/sidekiq/queue_throttled/middleware_spec.rb[1:2:2] | passed | 0.00013 seconds |
80
+ ./spec/sidekiq/queue_throttled/middleware_spec.rb[1:2:3] | passed | 0.0001 seconds |
81
+ ./spec/sidekiq/queue_throttled/middleware_spec.rb[1:3:1] | passed | 0.00015 seconds |
82
+ ./spec/sidekiq/queue_throttled/middleware_spec.rb[1:3:2] | passed | 0.00058 seconds |
83
+ ./spec/sidekiq/queue_throttled/middleware_spec.rb[1:3:3] | passed | 0.00023 seconds |
84
+ ./spec/sidekiq/queue_throttled/middleware_spec.rb[1:4:1] | passed | 0.0005 seconds |
85
+ ./spec/sidekiq/queue_throttled/middleware_spec.rb[1:4:2] | passed | 0.00098 seconds |
86
+ ./spec/sidekiq/queue_throttled/queue_limiter_spec.rb[1:1:1] | passed | 0.00013 seconds |
87
+ ./spec/sidekiq/queue_throttled/queue_limiter_spec.rb[1:1:2] | passed | 0.00014 seconds |
88
+ ./spec/sidekiq/queue_throttled/queue_limiter_spec.rb[1:1:3] | passed | 0.00016 seconds |
89
+ ./spec/sidekiq/queue_throttled/queue_limiter_spec.rb[1:2:1] | passed | 0.0004 seconds |
90
+ ./spec/sidekiq/queue_throttled/queue_limiter_spec.rb[1:2:2] | passed | 0.0011 seconds |
91
+ ./spec/sidekiq/queue_throttled/queue_limiter_spec.rb[1:2:3] | passed | 0.00088 seconds |
92
+ ./spec/sidekiq/queue_throttled/queue_limiter_spec.rb[1:2:4] | passed | 0.00046 seconds |
93
+ ./spec/sidekiq/queue_throttled/queue_limiter_spec.rb[1:2:5] | passed | 0.00058 seconds |
94
+ ./spec/sidekiq/queue_throttled/queue_limiter_spec.rb[1:3:1] | passed | 0.00058 seconds |
95
+ ./spec/sidekiq/queue_throttled/queue_limiter_spec.rb[1:3:2] | passed | 0.00014 seconds |
96
+ ./spec/sidekiq/queue_throttled/queue_limiter_spec.rb[1:3:3] | passed | 0.00016 seconds |
97
+ ./spec/sidekiq/queue_throttled/queue_limiter_spec.rb[1:3:4] | passed | 0.00038 seconds |
98
+ ./spec/sidekiq/queue_throttled/queue_limiter_spec.rb[1:4:1] | passed | 0.0002 seconds |
99
+ ./spec/sidekiq/queue_throttled/queue_limiter_spec.rb[1:4:2] | passed | 0.00098 seconds |
100
+ ./spec/sidekiq/queue_throttled/queue_limiter_spec.rb[1:4:3] | passed | 0.00095 seconds |
101
+ ./spec/sidekiq/queue_throttled/queue_limiter_spec.rb[1:5:1] | passed | 0.00021 seconds |
102
+ ./spec/sidekiq/queue_throttled/queue_limiter_spec.rb[1:5:2] | passed | 0.00045 seconds |
103
+ ./spec/sidekiq/queue_throttled/queue_limiter_spec.rb[1:5:3] | passed | 0.00112 seconds |
104
+ ./spec/sidekiq/queue_throttled/queue_limiter_spec.rb[1:5:4] | passed | 0.00023 seconds |
105
+ ./spec/sidekiq/queue_throttled/queue_limiter_spec.rb[1:6:1] | passed | 0.00081 seconds |
106
+ ./spec/sidekiq/queue_throttled/queue_limiter_spec.rb[1:6:2] | passed | 0.00152 seconds |
107
+ ./spec/sidekiq/queue_throttled/queue_limiter_spec.rb[1:7:1] | passed | 0.00151 seconds |
108
+ ./spec/sidekiq/queue_throttled/queue_limiter_spec.rb[1:7:2] | passed | 0.00136 seconds |
109
+ ./spec/sidekiq/queue_throttled/queue_limiter_spec.rb[1:8:1] | passed | 0.00066 seconds |
110
+ ./spec/sidekiq/queue_throttled/queue_limiter_spec.rb[1:8:2] | passed | 0.00093 seconds |
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Sidekiq::QueueThrottled::Configuration do
6
+ let(:config) { described_class.new }
7
+
8
+ describe '#set_queue_limit' do
9
+ it 'sets queue limit as integer' do
10
+ config.set_queue_limit(:test_queue, 100)
11
+ expect(config.queue_limit(:test_queue)).to eq(100)
12
+ end
13
+
14
+ it 'converts string limit to integer' do
15
+ config.set_queue_limit(:test_queue, '50')
16
+ expect(config.queue_limit(:test_queue)).to eq(50)
17
+ end
18
+
19
+ it 'handles string queue names' do
20
+ config.set_queue_limit('test_queue', 100)
21
+ expect(config.queue_limit('test_queue')).to eq(100)
22
+ end
23
+ end
24
+
25
+ describe '#queue_limit' do
26
+ it 'returns limit for string queue name' do
27
+ config.set_queue_limit('test_queue', 100)
28
+ expect(config.queue_limit('test_queue')).to eq(100)
29
+ end
30
+
31
+ it 'returns limit for symbol queue name' do
32
+ config.set_queue_limit(:test_queue, 100)
33
+ expect(config.queue_limit(:test_queue)).to eq(100)
34
+ end
35
+
36
+ it 'returns nil for non-existent queue' do
37
+ expect(config.queue_limit(:non_existent)).to be_nil
38
+ end
39
+ end
40
+
41
+ describe '#load_from_sidekiq_config!' do
42
+ let(:sidekiq_config) do
43
+ {
44
+ limits: {
45
+ 'queue1' => 100,
46
+ 'queue2' => 50
47
+ }
48
+ }
49
+ end
50
+
51
+ it 'loads limits from Sidekiq options' do
52
+ config.load_from_sidekiq_config!(sidekiq_config)
53
+ expect(config.queue_limit('queue1')).to eq(100)
54
+ expect(config.queue_limit('queue2')).to eq(50)
55
+ end
56
+
57
+ it 'handles string keys in limits' do
58
+ sidekiq_config_with_string_keys = {
59
+ 'limits' => {
60
+ 'queue1' => 100
61
+ }
62
+ }
63
+ config.load_from_sidekiq_config!(sidekiq_config_with_string_keys)
64
+ expect(config.queue_limit('queue1')).to eq(100)
65
+ end
66
+
67
+ it 'does nothing when limits are not defined' do
68
+ empty_config = {}
69
+ expect { config.load_from_sidekiq_config!(empty_config) }.not_to(change { config.queue_limits })
70
+ end
71
+ end
72
+
73
+ describe '#load_from_yaml!' do
74
+ let(:yaml_content) do
75
+ <<~YAML
76
+ limits:
77
+ queue1: 100
78
+ queue2: 50
79
+ YAML
80
+ end
81
+
82
+ it 'loads limits from YAML content' do
83
+ config.load_from_yaml!(yaml_content)
84
+ expect(config.queue_limit('queue1')).to eq(100)
85
+ expect(config.queue_limit('queue2')).to eq(50)
86
+ end
87
+
88
+ it 'handles string keys in YAML' do
89
+ yaml_with_string_keys = <<~YAML
90
+ "limits":
91
+ "queue1": 100
92
+ YAML
93
+ config.load_from_yaml!(yaml_with_string_keys)
94
+ expect(config.queue_limit('queue1')).to eq(100)
95
+ end
96
+
97
+ it 'does nothing when limits are not defined' do
98
+ yaml_without_limits = <<~YAML
99
+ other_config: value
100
+ YAML
101
+ expect { config.load_from_yaml!(yaml_without_limits) }.not_to(change { config.queue_limits })
102
+ end
103
+ end
104
+
105
+ describe '#validate!' do
106
+ it 'passes validation for valid limits' do
107
+ config.set_queue_limit(:queue1, 100)
108
+ config.set_queue_limit(:queue2, 50)
109
+ expect { config.validate! }.not_to raise_error
110
+ end
111
+
112
+ it 'raises error for non-positive limits' do
113
+ config.set_queue_limit(:queue1, 0)
114
+ expect { config.validate! }.to raise_error(ArgumentError, /positive integer/)
115
+ end
116
+
117
+ it 'raises error for negative limits' do
118
+ config.set_queue_limit(:queue1, -1)
119
+ expect { config.validate! }.to raise_error(ArgumentError, /positive integer/)
120
+ end
121
+
122
+ it 'raises error for non-integer limits' do
123
+ config.queue_limits[:queue1] = 'invalid'
124
+ expect { config.validate! }.to raise_error(ArgumentError, /positive integer/)
125
+ end
126
+ end
127
+
128
+ describe 'default values' do
129
+ it 'has correct default redis_key_prefix' do
130
+ expect(config.redis_key_prefix).to eq('sidekiq:queue_throttled')
131
+ end
132
+
133
+ it 'has correct default throttle_ttl' do
134
+ expect(config.throttle_ttl).to eq(3600)
135
+ end
136
+
137
+ it 'has correct default lock_ttl' do
138
+ expect(config.lock_ttl).to eq(300)
139
+ end
140
+
141
+ it 'has correct default retry_delay' do
142
+ expect(config.retry_delay).to eq(5)
143
+ end
144
+ end
145
+ end