HornsAndHooves-sidekiq-limit_fetch 4.5.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 +7 -0
- data/.gitignore +4 -0
- data/.rspec +5 -0
- data/.rubocop.yml +34 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +37 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +165 -0
- data/Rakefile +14 -0
- data/bench/compare.rb +56 -0
- data/demo/Gemfile +8 -0
- data/demo/README.md +37 -0
- data/demo/Rakefile +100 -0
- data/demo/app/workers/a_worker.rb +10 -0
- data/demo/app/workers/b_worker.rb +10 -0
- data/demo/app/workers/c_worker.rb +10 -0
- data/demo/app/workers/fast_worker.rb +10 -0
- data/demo/app/workers/slow_worker.rb +10 -0
- data/demo/config/application.rb +13 -0
- data/demo/config/boot.rb +4 -0
- data/demo/config/environment.rb +4 -0
- data/demo/config/environments/development.rb +11 -0
- data/lib/sidekiq/extensions/manager.rb +21 -0
- data/lib/sidekiq/extensions/queue.rb +27 -0
- data/lib/sidekiq/limit_fetch/global/monitor.rb +83 -0
- data/lib/sidekiq/limit_fetch/global/selector.rb +130 -0
- data/lib/sidekiq/limit_fetch/global/semaphore.rb +190 -0
- data/lib/sidekiq/limit_fetch/instances.rb +29 -0
- data/lib/sidekiq/limit_fetch/queues.rb +197 -0
- data/lib/sidekiq/limit_fetch/unit_of_work.rb +28 -0
- data/lib/sidekiq/limit_fetch.rb +76 -0
- data/lib/sidekiq-limit_fetch.rb +3 -0
- data/sidekiq-limit_fetch.gemspec +30 -0
- data/spec/sidekiq/extensions/manager_spec.rb +13 -0
- data/spec/sidekiq/extensions/queue_spec.rb +96 -0
- data/spec/sidekiq/limit_fetch/global/monitor_spec.rb +114 -0
- data/spec/sidekiq/limit_fetch/queues_spec.rb +127 -0
- data/spec/sidekiq/limit_fetch/semaphore_spec.rb +65 -0
- data/spec/sidekiq/limit_fetch_spec.rb +58 -0
- data/spec/spec_helper.rb +34 -0
- metadata +179 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'forwardable'
|
|
4
|
+
require 'sidekiq'
|
|
5
|
+
require 'sidekiq/manager'
|
|
6
|
+
require 'sidekiq/api'
|
|
7
|
+
|
|
8
|
+
module Sidekiq
|
|
9
|
+
module LimitFetch
|
|
10
|
+
autoload :UnitOfWork, 'sidekiq/limit_fetch/unit_of_work'
|
|
11
|
+
|
|
12
|
+
require_relative 'limit_fetch/instances'
|
|
13
|
+
require_relative 'limit_fetch/queues'
|
|
14
|
+
require_relative 'limit_fetch/global/semaphore'
|
|
15
|
+
require_relative 'limit_fetch/global/selector'
|
|
16
|
+
require_relative 'limit_fetch/global/monitor'
|
|
17
|
+
require_relative 'extensions/queue'
|
|
18
|
+
require_relative 'extensions/manager'
|
|
19
|
+
|
|
20
|
+
TIMEOUT = Sidekiq::BasicFetch::TIMEOUT
|
|
21
|
+
|
|
22
|
+
extend self
|
|
23
|
+
|
|
24
|
+
RedisBaseConnectionError = RedisClient::ConnectionError
|
|
25
|
+
RedisCommandError = RedisClient::CommandError
|
|
26
|
+
|
|
27
|
+
def new(_)
|
|
28
|
+
self
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def retrieve_work
|
|
32
|
+
queue, job = redis_brpop(Queues.acquire)
|
|
33
|
+
Queues.release_except(queue)
|
|
34
|
+
UnitOfWork.new(queue, job, capsule) if job
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def capsule
|
|
38
|
+
Sidekiq.default_configuration.default_capsule
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def bulk_requeue(*args)
|
|
42
|
+
Sidekiq::BasicFetch.new(capsule).bulk_requeue(*args)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def redis_retryable
|
|
46
|
+
yield
|
|
47
|
+
rescue RedisBaseConnectionError
|
|
48
|
+
sleep TIMEOUT
|
|
49
|
+
retry
|
|
50
|
+
rescue RedisCommandError => e
|
|
51
|
+
# If Redis was restarted and is still loading its snapshot,
|
|
52
|
+
# then we should treat this as a temporary connection error too.
|
|
53
|
+
raise unless e.message =~ /^LOADING/
|
|
54
|
+
|
|
55
|
+
sleep TIMEOUT
|
|
56
|
+
retry
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
# rubocop:disable Metrics/MethodLength
|
|
62
|
+
def redis_brpop(queues)
|
|
63
|
+
if queues.empty?
|
|
64
|
+
sleep TIMEOUT # there are no queues to handle, so lets sleep
|
|
65
|
+
[] # and return nothing
|
|
66
|
+
else
|
|
67
|
+
redis_retryable do
|
|
68
|
+
Sidekiq.redis do |it|
|
|
69
|
+
it.blocking_call(false, 'brpop', *queues, TIMEOUT)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
# rubocop:enable Metrics/MethodLength
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'English'
|
|
4
|
+
Gem::Specification.new do |gem|
|
|
5
|
+
gem.name = 'HornsAndHooves-sidekiq-limit_fetch'
|
|
6
|
+
gem.version = '4.5.0'
|
|
7
|
+
gem.license = 'MIT'
|
|
8
|
+
gem.authors = ['HornsAndHooves', 'Peter Maneykowski']
|
|
9
|
+
gem.email = ['maneyko@integracredit.com']
|
|
10
|
+
gem.summary = 'Sidekiq strategy to support queue limits'
|
|
11
|
+
gem.homepage = 'https://github.com/HornsAndHooves/sidekiq-limit_fetch'
|
|
12
|
+
gem.description = 'Sidekiq strategy to restrict number of workers which are able to run specified ' \
|
|
13
|
+
'queues simultaneously.'
|
|
14
|
+
|
|
15
|
+
gem.metadata['homepage_uri'] = gem.homepage
|
|
16
|
+
gem.metadata['source_code_uri'] = 'https://github.com/HornsAndHooves/sidekiq-limit_fetch'
|
|
17
|
+
gem.metadata['changelog_uri'] = 'https://github.com/HornsAndHooves/sidekiq-limit_fetch/blob/master/CHANGELOG.md'
|
|
18
|
+
|
|
19
|
+
gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
|
|
20
|
+
gem.require_paths = %w[lib]
|
|
21
|
+
|
|
22
|
+
gem.required_ruby_version = '>= 2.7.0'
|
|
23
|
+
|
|
24
|
+
gem.add_dependency 'sidekiq', '>= 8'
|
|
25
|
+
gem.add_development_dependency 'rake'
|
|
26
|
+
gem.add_development_dependency 'redis-namespace', '~> 1.5', '>= 1.5.2'
|
|
27
|
+
gem.add_development_dependency 'rspec'
|
|
28
|
+
gem.add_development_dependency 'rubocop'
|
|
29
|
+
gem.add_development_dependency 'simplecov'
|
|
30
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Sidekiq::Manager do
|
|
4
|
+
let(:capsule_or_options) do
|
|
5
|
+
Sidekiq.default_configuration.default_capsule
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
it 'can be instantiated' do
|
|
9
|
+
expect(described_class).to be < Sidekiq::Manager::InitLimitFetch
|
|
10
|
+
manager = described_class.new(capsule_or_options)
|
|
11
|
+
expect(manager).to respond_to(:start)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Sidekiq::Queue do
|
|
4
|
+
context 'singleton' do
|
|
5
|
+
shared_examples :constructor do
|
|
6
|
+
it 'with default name' do
|
|
7
|
+
new_object = -> { described_class.send constructor }
|
|
8
|
+
expect(new_object.call).to eq new_object.call
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it 'with given name' do
|
|
12
|
+
new_object = ->(name) { described_class.send constructor, name }
|
|
13
|
+
expect(new_object.call('name')).to eq new_object.call('name')
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
context '.new' do
|
|
18
|
+
let(:constructor) { :new }
|
|
19
|
+
it_behaves_like :constructor
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
context '.[]' do
|
|
23
|
+
let(:constructor) { :[] }
|
|
24
|
+
it_behaves_like :constructor
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
context '#lock' do
|
|
28
|
+
let(:name) { 'example' }
|
|
29
|
+
let(:queue) { Sidekiq::Queue[name] }
|
|
30
|
+
|
|
31
|
+
it 'should be available' do
|
|
32
|
+
expect(queue.acquire).to be
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'should be pausable' do
|
|
36
|
+
queue.pause
|
|
37
|
+
expect(queue.acquire).not_to be
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'should be continuable' do
|
|
41
|
+
queue.pause
|
|
42
|
+
queue.unpause
|
|
43
|
+
expect(queue.acquire).to be
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it 'should be limitable' do
|
|
47
|
+
queue.limit = 1
|
|
48
|
+
expect(queue.acquire).to be
|
|
49
|
+
expect(queue.acquire).not_to be
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'should be resizable' do
|
|
53
|
+
queue.limit = 0
|
|
54
|
+
expect(queue.acquire).not_to be
|
|
55
|
+
queue.limit = nil
|
|
56
|
+
expect(queue.acquire).to be
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it 'should be countable' do
|
|
60
|
+
queue.limit = 3
|
|
61
|
+
5.times { queue.acquire }
|
|
62
|
+
expect(queue.probed).to eq 3
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it 'should be releasable' do
|
|
66
|
+
queue.acquire
|
|
67
|
+
expect(queue.probed).to eq 1
|
|
68
|
+
queue.release
|
|
69
|
+
expect(queue.probed).to eq 0
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it 'should tell if paused' do
|
|
73
|
+
expect(queue).not_to be_paused
|
|
74
|
+
queue.pause
|
|
75
|
+
expect(queue).to be_paused
|
|
76
|
+
queue.unpause
|
|
77
|
+
expect(queue).not_to be_paused
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it 'should tell if blocking' do
|
|
81
|
+
expect(queue).not_to be_blocking
|
|
82
|
+
queue.block
|
|
83
|
+
expect(queue).to be_blocking
|
|
84
|
+
queue.unblock
|
|
85
|
+
expect(queue).not_to be_blocking
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it 'should be marked as changed' do
|
|
89
|
+
queue = Sidekiq::Queue["uniq_#{name}"]
|
|
90
|
+
expect(queue).not_to be_limit_changed
|
|
91
|
+
queue.limit = 3
|
|
92
|
+
expect(queue).to be_limit_changed
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Sidekiq::LimitFetch::Global::Monitor do
|
|
4
|
+
let(:monitor) { described_class.start! ttl, timeout }
|
|
5
|
+
let(:ttl) { 1 }
|
|
6
|
+
let(:queue) { Sidekiq::Queue[name] }
|
|
7
|
+
let(:name) { 'default' }
|
|
8
|
+
let(:timeout) { 0.5 }
|
|
9
|
+
|
|
10
|
+
after { monitor.kill }
|
|
11
|
+
|
|
12
|
+
context 'old locks' do
|
|
13
|
+
before { monitor }
|
|
14
|
+
|
|
15
|
+
it 'should remove invalidated old locks' do
|
|
16
|
+
2.times { queue.acquire }
|
|
17
|
+
sleep ttl * 2
|
|
18
|
+
expect(queue.probed).to eq 2
|
|
19
|
+
|
|
20
|
+
allow(described_class).to receive(:update_heartbeat)
|
|
21
|
+
sleep ttl * 2
|
|
22
|
+
expect(queue.probed).to eq 0
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it 'should remove invalid locks' do
|
|
26
|
+
2.times { queue.acquire }
|
|
27
|
+
allow(described_class).to receive(:update_heartbeat)
|
|
28
|
+
Sidekiq.redis do |it|
|
|
29
|
+
it.del Sidekiq::LimitFetch::Global::Monitor::PROCESS_SET
|
|
30
|
+
end
|
|
31
|
+
sleep ttl * 2
|
|
32
|
+
expect(queue.probed).to eq 0
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
context 'dynamic queue' do
|
|
37
|
+
let(:limits) do
|
|
38
|
+
{
|
|
39
|
+
'queue1' => 3,
|
|
40
|
+
'queue2' => 3
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
let(:queues) { %w[queue1 queue2] }
|
|
44
|
+
let(:queue) { Sidekiq::LimitFetch::Queues }
|
|
45
|
+
|
|
46
|
+
let(:config) { Sidekiq::Config.new(options) }
|
|
47
|
+
let(:capsule) do
|
|
48
|
+
config.capsule('default') do |cap|
|
|
49
|
+
cap.concurrency = 1
|
|
50
|
+
cap.queues = config[:queues]
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
let(:capsule_or_options) do
|
|
55
|
+
capsule
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
context 'without excluded queue' do
|
|
59
|
+
let(:options) do
|
|
60
|
+
{
|
|
61
|
+
limits: limits,
|
|
62
|
+
queues: queues,
|
|
63
|
+
dynamic: true
|
|
64
|
+
}
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it 'should add dynamic queue' do
|
|
68
|
+
queue.start(capsule_or_options)
|
|
69
|
+
monitor
|
|
70
|
+
|
|
71
|
+
expect(queue.instance_variable_get(:@queues)).not_to include('queue3')
|
|
72
|
+
|
|
73
|
+
Sidekiq.redis do |it|
|
|
74
|
+
it.sadd 'queues', 'queue3'
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
sleep ttl * 2
|
|
78
|
+
expect(queue.instance_variable_get(:@queues)).to include('queue3')
|
|
79
|
+
|
|
80
|
+
Sidekiq.redis do |it|
|
|
81
|
+
it.srem 'queues', 'queue3'
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
context 'with excluded queue' do
|
|
87
|
+
let(:options) do
|
|
88
|
+
{
|
|
89
|
+
limits: limits,
|
|
90
|
+
queues: queues,
|
|
91
|
+
dynamic: { exclude: ['queue4'] }
|
|
92
|
+
}
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it 'should exclude excluded dynamic queue' do
|
|
96
|
+
queue.start(capsule_or_options)
|
|
97
|
+
monitor
|
|
98
|
+
|
|
99
|
+
expect(queue.instance_variable_get(:@queues)).not_to include('queue4')
|
|
100
|
+
|
|
101
|
+
Sidekiq.redis do |it|
|
|
102
|
+
it.sadd 'queues', 'queue4'
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
sleep ttl * 2
|
|
106
|
+
expect(queue.instance_variable_get(:@queues)).not_to include('queue4')
|
|
107
|
+
|
|
108
|
+
Sidekiq.redis do |it|
|
|
109
|
+
it.srem 'queues', 'queue4'
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Sidekiq::LimitFetch::Queues do
|
|
4
|
+
let(:queues) { %w[queue1 queue2] }
|
|
5
|
+
let(:limits) { { 'queue1' => 3 } }
|
|
6
|
+
let(:strict) { true }
|
|
7
|
+
let(:blocking) { nil }
|
|
8
|
+
let(:process_limits) { { 'queue2' => 3 } }
|
|
9
|
+
|
|
10
|
+
let(:options) do
|
|
11
|
+
{ queues: queues,
|
|
12
|
+
limits: limits,
|
|
13
|
+
strict: strict,
|
|
14
|
+
blocking: blocking,
|
|
15
|
+
process_limits: process_limits }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
let(:config) { Sidekiq::Config.new(options) }
|
|
19
|
+
let(:capsule) do
|
|
20
|
+
config.capsule('default') do |cap|
|
|
21
|
+
cap.concurrency = 1
|
|
22
|
+
cap.queues = config[:queues]
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
let(:capsule_or_options) do
|
|
27
|
+
capsule
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
before do
|
|
31
|
+
subject.start(capsule_or_options)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def in_thread(&block)
|
|
35
|
+
thr = Thread.new(&block)
|
|
36
|
+
thr.join
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it 'should acquire queues' do
|
|
40
|
+
in_thread { subject.acquire }
|
|
41
|
+
expect(Sidekiq::Queue['queue1'].probed).to eq 1
|
|
42
|
+
expect(Sidekiq::Queue['queue2'].probed).to eq 1
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it 'should acquire dynamically blocking queues' do
|
|
46
|
+
in_thread { subject.acquire }
|
|
47
|
+
expect(Sidekiq::Queue['queue1'].probed).to eq 1
|
|
48
|
+
expect(Sidekiq::Queue['queue2'].probed).to eq 1
|
|
49
|
+
|
|
50
|
+
Sidekiq::Queue['queue1'].block
|
|
51
|
+
|
|
52
|
+
in_thread { subject.acquire }
|
|
53
|
+
expect(Sidekiq::Queue['queue1'].probed).to eq 2
|
|
54
|
+
expect(Sidekiq::Queue['queue2'].probed).to eq 1
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it 'should block except given queues' do
|
|
58
|
+
Sidekiq::Queue['queue1'].block_except 'queue2'
|
|
59
|
+
in_thread { subject.acquire }
|
|
60
|
+
expect(Sidekiq::Queue['queue1'].probed).to eq 1
|
|
61
|
+
expect(Sidekiq::Queue['queue2'].probed).to eq 1
|
|
62
|
+
|
|
63
|
+
Sidekiq::Queue['queue1'].block_except 'queue404'
|
|
64
|
+
in_thread { subject.acquire }
|
|
65
|
+
expect(Sidekiq::Queue['queue1'].probed).to eq 2
|
|
66
|
+
expect(Sidekiq::Queue['queue2'].probed).to eq 1
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it 'should release queues' do
|
|
70
|
+
in_thread do
|
|
71
|
+
subject.acquire
|
|
72
|
+
subject.release_except nil
|
|
73
|
+
end
|
|
74
|
+
expect(Sidekiq::Queue['queue1'].probed).to eq 0
|
|
75
|
+
expect(Sidekiq::Queue['queue2'].probed).to eq 0
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it 'should release queues except selected' do
|
|
79
|
+
in_thread do
|
|
80
|
+
subject.acquire
|
|
81
|
+
subject.release_except 'queue:queue1'
|
|
82
|
+
end
|
|
83
|
+
expect(Sidekiq::Queue['queue1'].probed).to eq 1
|
|
84
|
+
expect(Sidekiq::Queue['queue2'].probed).to eq 0
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it 'should release when no queues was acquired' do
|
|
88
|
+
queues.each { |name| Sidekiq::Queue[name].pause }
|
|
89
|
+
in_thread do
|
|
90
|
+
subject.acquire
|
|
91
|
+
expect { subject.release_except nil }.not_to raise_exception
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
context 'blocking' do
|
|
96
|
+
let(:blocking) { %w[queue1] }
|
|
97
|
+
|
|
98
|
+
it 'should acquire blocking queues' do
|
|
99
|
+
3.times { in_thread { subject.acquire } }
|
|
100
|
+
expect(Sidekiq::Queue['queue1'].probed).to eq 3
|
|
101
|
+
expect(Sidekiq::Queue['queue2'].probed).to eq 1
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it 'should set limits' do
|
|
106
|
+
subject
|
|
107
|
+
expect(Sidekiq::Queue['queue1'].limit).to eq 3
|
|
108
|
+
expect(Sidekiq::Queue['queue2'].limit).not_to be
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it 'should set process_limits' do
|
|
112
|
+
subject
|
|
113
|
+
expect(Sidekiq::Queue['queue2'].process_limit).to eq 3
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
context 'without strict flag' do
|
|
117
|
+
let(:strict) { false }
|
|
118
|
+
|
|
119
|
+
it 'should retrieve weighted queues' do
|
|
120
|
+
expect(subject.ordered_queues).to match_array(%w[queue1 queue2])
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it 'with strict flag should retrieve strictly ordered queues' do
|
|
125
|
+
expect(subject.ordered_queues).to eq %w[queue1 queue2]
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe 'semaphore' do
|
|
4
|
+
let(:name) { 'default' }
|
|
5
|
+
subject { Sidekiq::LimitFetch::Global::Semaphore.new name }
|
|
6
|
+
|
|
7
|
+
it 'should have no limit by default' do
|
|
8
|
+
expect(subject.limit).not_to be
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it 'should set limit' do
|
|
12
|
+
subject.limit = 4
|
|
13
|
+
expect(subject.limit).to eq 4
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'should acquire and count active tasks' do
|
|
17
|
+
3.times { subject.acquire }
|
|
18
|
+
expect(subject.probed).to eq 3
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'should acquire tasks with regard to limit' do
|
|
22
|
+
subject.limit = 4
|
|
23
|
+
6.times { subject.acquire }
|
|
24
|
+
expect(subject.probed).to eq 4
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'should acquire tasks with regard to process limit' do
|
|
28
|
+
subject.process_limit = 4
|
|
29
|
+
6.times { subject.acquire }
|
|
30
|
+
expect(subject.probed).to eq 4
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'should release active tasks' do
|
|
34
|
+
6.times { subject.acquire }
|
|
35
|
+
3.times { subject.release }
|
|
36
|
+
expect(subject.probed).to eq 3
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it 'should pause tasks' do
|
|
40
|
+
3.times { subject.acquire }
|
|
41
|
+
subject.pause
|
|
42
|
+
2.times { subject.acquire }
|
|
43
|
+
expect(subject.probed).to eq 3
|
|
44
|
+
2.times { subject.release }
|
|
45
|
+
expect(subject.probed).to eq 1
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'should unpause tasks' do
|
|
49
|
+
subject.pause
|
|
50
|
+
3.times { subject.acquire }
|
|
51
|
+
subject.unpause
|
|
52
|
+
2.times { subject.acquire }
|
|
53
|
+
expect(subject.probed).to eq 2
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it 'should pause tasks for a limited time' do
|
|
57
|
+
3.times { subject.acquire }
|
|
58
|
+
subject.pause_for_ms 50
|
|
59
|
+
2.times { subject.acquire }
|
|
60
|
+
expect(subject.probed).to eq 3
|
|
61
|
+
sleep(100.0 / 1000)
|
|
62
|
+
2.times { subject.acquire }
|
|
63
|
+
expect(subject.probed).to eq 5
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Thread.abort_on_exception = true
|
|
4
|
+
|
|
5
|
+
RSpec.describe Sidekiq::LimitFetch do
|
|
6
|
+
let(:options) { { queues: queues, limits: limits } }
|
|
7
|
+
let(:queues) { %w[queue1 queue1 queue2 queue2] }
|
|
8
|
+
let(:limits) { { 'queue1' => 1, 'queue2' => 2 } }
|
|
9
|
+
let(:config) { Sidekiq::Config.new(options) }
|
|
10
|
+
let(:capsule) do
|
|
11
|
+
config.capsule('default') do |cap|
|
|
12
|
+
cap.concurrency = 1
|
|
13
|
+
cap.queues = config[:queues]
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
let(:capsule_or_config) { capsule }
|
|
17
|
+
|
|
18
|
+
before do
|
|
19
|
+
subject::Queues.start(capsule_or_config)
|
|
20
|
+
|
|
21
|
+
Sidekiq.redis do |it|
|
|
22
|
+
it.del 'queue:queue1'
|
|
23
|
+
it.lpush 'queue:queue1', 'task1'
|
|
24
|
+
it.lpush 'queue:queue1', 'task2'
|
|
25
|
+
it.expire 'queue:queue1', 30
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'should acquire lock on queue for execution' do
|
|
30
|
+
work = subject.retrieve_work
|
|
31
|
+
expect(work.queue_name).to eq 'queue1'
|
|
32
|
+
expect(work.job).to eq 'task1'
|
|
33
|
+
|
|
34
|
+
expect(Sidekiq::Queue['queue1'].busy).to eq 1
|
|
35
|
+
expect(Sidekiq::Queue['queue2'].busy).to eq 0
|
|
36
|
+
|
|
37
|
+
expect(subject.retrieve_work).not_to be
|
|
38
|
+
work.requeue
|
|
39
|
+
|
|
40
|
+
expect(Sidekiq::Queue['queue1'].busy).to eq 0
|
|
41
|
+
expect(Sidekiq::Queue['queue2'].busy).to eq 0
|
|
42
|
+
|
|
43
|
+
work = subject.retrieve_work
|
|
44
|
+
expect(work.job).to eq 'task1'
|
|
45
|
+
|
|
46
|
+
expect(Sidekiq::Queue['queue1'].busy).to eq 1
|
|
47
|
+
expect(Sidekiq::Queue['queue2'].busy).to eq 0
|
|
48
|
+
|
|
49
|
+
expect(subject.retrieve_work).not_to be
|
|
50
|
+
work.acknowledge
|
|
51
|
+
|
|
52
|
+
expect(Sidekiq::Queue['queue1'].busy).to eq 0
|
|
53
|
+
expect(Sidekiq::Queue['queue2'].busy).to eq 0
|
|
54
|
+
|
|
55
|
+
work = subject.retrieve_work
|
|
56
|
+
expect(work.job).to eq 'task2'
|
|
57
|
+
end
|
|
58
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'simplecov'
|
|
4
|
+
SimpleCov.start
|
|
5
|
+
|
|
6
|
+
require 'sidekiq/limit_fetch'
|
|
7
|
+
|
|
8
|
+
Sidekiq.configure_embed do |config|
|
|
9
|
+
config.logger = nil
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
RSpec.configure do |config|
|
|
13
|
+
config.order = :random
|
|
14
|
+
config.disable_monkey_patching!
|
|
15
|
+
config.raise_errors_for_deprecations!
|
|
16
|
+
config.before do
|
|
17
|
+
Sidekiq::Queue.reset_instances!
|
|
18
|
+
Sidekiq.redis do |it|
|
|
19
|
+
clean_redis = lambda do |queue|
|
|
20
|
+
it.pipelined do |pipeline|
|
|
21
|
+
pipeline.del "limit_fetch:limit:#{queue}"
|
|
22
|
+
pipeline.del "limit_fetch:process_limit:#{queue}"
|
|
23
|
+
pipeline.del "limit_fetch:busy:#{queue}"
|
|
24
|
+
pipeline.del "limit_fetch:probed:#{queue}"
|
|
25
|
+
pipeline.del "limit_fetch:pause:#{queue}"
|
|
26
|
+
pipeline.del "limit_fetch:block:#{queue}"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
clean_redis.call(name) if defined?(name)
|
|
31
|
+
queues.each(&clean_redis) if defined?(queues) && queues.is_a?(Array)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|