sidekiq-limit_fetch 0.4 → 0.6
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.
- data/README.md +17 -2
- data/lib/sidekiq/extensions/queue.rb +19 -0
- data/lib/sidekiq/limit_fetch.rb +15 -43
- data/lib/sidekiq/limit_fetch/global/selector.rb +92 -0
- data/lib/sidekiq/limit_fetch/global/semaphore.rb +38 -0
- data/lib/sidekiq/limit_fetch/local/selector.rb +19 -0
- data/lib/sidekiq/limit_fetch/local/semaphore.rb +39 -0
- data/lib/sidekiq/limit_fetch/queues.rb +57 -0
- data/lib/sidekiq/limit_fetch/unit_of_work.rb +9 -13
- data/sidekiq-limit_fetch.gemspec +2 -2
- data/spec/sidekiq/extensions/queue_spec.rb +85 -0
- data/spec/sidekiq/limit_fetch/queues_spec.rb +75 -0
- data/spec/sidekiq/limit_fetch/semaphore_spec.rb +68 -0
- data/spec/sidekiq/limit_fetch_spec.rb +47 -0
- data/spec/spec_helper.rb +12 -0
- metadata +26 -18
- data/lib/sidekiq/limit_fetch/queue.rb +0 -14
- data/lib/sidekiq/limit_fetch/semaphore.rb +0 -26
- data/spec/limit_fetch_spec.rb +0 -81
- data/spec/sidekiq/limit_fetch/queue_spec.rb +0 -27
data/README.md
CHANGED
@@ -21,7 +21,7 @@ Specify limits which you want to place on queues inside sidekiq.yml:
|
|
21
21
|
|
22
22
|
Or set it dynamically in your code:
|
23
23
|
```ruby
|
24
|
-
Sidekiq::Queue
|
24
|
+
Sidekiq::Queue['queue_name1'].limit = 5
|
25
25
|
Sidekiq::Queue['queue_name2'].limit = 10
|
26
26
|
```
|
27
27
|
|
@@ -32,7 +32,22 @@ workers simultaneously.
|
|
32
32
|
Ability to set limits dynamically allows you to resize worker
|
33
33
|
distribution among queues any time you want.
|
34
34
|
|
35
|
-
|
35
|
+
You can also pause your queues temporarely. Upon continuing their limits
|
36
|
+
will be preserved.
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
Sidekiq::Queue['name'].pause # prevents workers from running tasks from this queue
|
40
|
+
...
|
41
|
+
Sidekiq::Queue['name'].continue # allows workers to use the queue with the same limit
|
42
|
+
```
|
43
|
+
|
44
|
+
Limits are applied per process. In case you have several worker
|
45
|
+
processes and want to have global locks between them, you'll need to
|
46
|
+
enable global mode by setting global option, eg:
|
47
|
+
|
48
|
+
```yaml
|
49
|
+
:global: true
|
50
|
+
```
|
36
51
|
|
37
52
|
Sponsored by [Evil Martians].
|
38
53
|
[Evil Martians]: http://evilmartians.com/
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Sidekiq
|
2
|
+
class Queue
|
3
|
+
extend LimitFetch::Singleton, Forwardable
|
4
|
+
|
5
|
+
def_delegators :lock,
|
6
|
+
:limit, :limit=,
|
7
|
+
:acquire, :release,
|
8
|
+
:pause, :continue,
|
9
|
+
:busy
|
10
|
+
|
11
|
+
def lock
|
12
|
+
@lock ||= mode::Semaphore.new name
|
13
|
+
end
|
14
|
+
|
15
|
+
def mode
|
16
|
+
Sidekiq.options[:global] ? LimitFetch::Global : LimitFetch::Local
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/sidekiq/limit_fetch.rb
CHANGED
@@ -2,10 +2,14 @@ require 'sidekiq'
|
|
2
2
|
require 'sidekiq/fetch'
|
3
3
|
|
4
4
|
class Sidekiq::LimitFetch
|
5
|
-
require_relative 'limit_fetch/semaphore'
|
6
5
|
require_relative 'limit_fetch/unit_of_work'
|
7
6
|
require_relative 'limit_fetch/singleton'
|
8
|
-
require_relative 'limit_fetch/
|
7
|
+
require_relative 'limit_fetch/queues'
|
8
|
+
require_relative 'limit_fetch/local/semaphore'
|
9
|
+
require_relative 'limit_fetch/local/selector'
|
10
|
+
require_relative 'limit_fetch/global/semaphore'
|
11
|
+
require_relative 'limit_fetch/global/selector'
|
12
|
+
require_relative 'extensions/queue'
|
9
13
|
|
10
14
|
Sidekiq.options[:fetch] = self
|
11
15
|
|
@@ -14,55 +18,23 @@ class Sidekiq::LimitFetch
|
|
14
18
|
end
|
15
19
|
|
16
20
|
def initialize(options)
|
17
|
-
|
18
|
-
options[:strict] ? define_strict_queues : define_weighted_queues
|
19
|
-
end
|
20
|
-
|
21
|
-
def available_queues
|
22
|
-
fetch_queues.select(&:acquire)
|
21
|
+
@queues = Queues.new options
|
23
22
|
end
|
24
23
|
|
25
24
|
def retrieve_work
|
26
|
-
|
27
|
-
|
28
|
-
if queues.empty?
|
29
|
-
sleep Sidekiq::Fetcher::TIMEOUT
|
30
|
-
return
|
31
|
-
end
|
32
|
-
|
33
|
-
queue_name, message = Sidekiq.redis do |it|
|
34
|
-
it.brpop *queues.map(&:full_name), Sidekiq::Fetcher::TIMEOUT
|
35
|
-
end
|
36
|
-
|
37
|
-
if message
|
38
|
-
queue = queues.delete queues.find {|it| it.full_name == queue_name }
|
39
|
-
UnitOfWork.new queue, message
|
40
|
-
end
|
41
|
-
ensure
|
42
|
-
queues.each(&:release) if queues
|
25
|
+
queue, message = fetch_message
|
26
|
+
UnitOfWork.new queue, message if message
|
43
27
|
end
|
44
28
|
|
45
29
|
private
|
46
30
|
|
47
|
-
def
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
it.limit = limits[name] if limits[name]
|
52
|
-
end
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
def define_strict_queues
|
57
|
-
@queues.uniq!
|
58
|
-
def fetch_queues
|
59
|
-
@queues
|
60
|
-
end
|
31
|
+
def fetch_message
|
32
|
+
queue, _ = redis_blpop *@queues.acquire, Sidekiq::Fetcher::TIMEOUT
|
33
|
+
ensure
|
34
|
+
@queues.release_except queue
|
61
35
|
end
|
62
36
|
|
63
|
-
def
|
64
|
-
|
65
|
-
@queues.shuffle.uniq
|
66
|
-
end
|
37
|
+
def redis_blpop(*args)
|
38
|
+
Sidekiq.redis {|it| it.blpop *args }
|
67
39
|
end
|
68
40
|
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module Sidekiq::LimitFetch::Global
|
2
|
+
module Selector
|
3
|
+
extend self
|
4
|
+
|
5
|
+
def acquire(queues)
|
6
|
+
redis_eval :acquire, [namespace, uuid, queues]
|
7
|
+
end
|
8
|
+
|
9
|
+
def release(queues)
|
10
|
+
redis_eval :release, [namespace, uuid, queues]
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def namespace
|
16
|
+
@namespace ||= begin
|
17
|
+
namespace = Sidekiq.options[:namespace]
|
18
|
+
namespace + ':' if namespace
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def uuid
|
23
|
+
@uuid ||= SecureRandom.uuid
|
24
|
+
end
|
25
|
+
|
26
|
+
def redis_eval(script_name, args)
|
27
|
+
Sidekiq.redis do |it|
|
28
|
+
begin
|
29
|
+
it.evalsha send("redis_#{script_name}_sha"), argv: args
|
30
|
+
rescue Redis::CommandError => error
|
31
|
+
raise unless error.message.include? 'NOSCRIPT'
|
32
|
+
it.eval send("redis_#{script_name}_script"), argv: args
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def redis_acquire_sha
|
38
|
+
@acquire_sha ||= Digest::SHA1.hexdigest redis_acquire_script
|
39
|
+
end
|
40
|
+
|
41
|
+
def redis_release_sha
|
42
|
+
@release_sha ||= Digest::SHA1.hexdigest redis_release_script
|
43
|
+
end
|
44
|
+
|
45
|
+
def redis_acquire_script
|
46
|
+
<<-LUA
|
47
|
+
local namespace = table.remove(ARGV, 1)..'limit_fetch:'
|
48
|
+
local worker_name = table.remove(ARGV, 1)
|
49
|
+
local queues = ARGV
|
50
|
+
local available = {}
|
51
|
+
|
52
|
+
for _, queue in ipairs(queues) do
|
53
|
+
local busy_key = namespace..'busy:'..queue
|
54
|
+
local pause_key = namespace..'pause:'..queue
|
55
|
+
local paused = redis.call('get', pause_key)
|
56
|
+
|
57
|
+
if not paused then
|
58
|
+
local limit_key = namespace..'limit:'..queue
|
59
|
+
local queue_limit = tonumber(redis.call('get', limit_key))
|
60
|
+
|
61
|
+
if queue_limit then
|
62
|
+
local queue_locks = redis.call('llen', busy_key)
|
63
|
+
|
64
|
+
if queue_limit > queue_locks then
|
65
|
+
redis.call('rpush', busy_key, worker_name)
|
66
|
+
table.insert(available, queue)
|
67
|
+
end
|
68
|
+
else
|
69
|
+
redis.call('rpush', busy_key, worker_name)
|
70
|
+
table.insert(available, queue)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
return available
|
76
|
+
LUA
|
77
|
+
end
|
78
|
+
|
79
|
+
def redis_release_script
|
80
|
+
<<-LUA
|
81
|
+
local namespace = table.remove(ARGV, 1)..'limit_fetch:'
|
82
|
+
local worker_name = table.remove(ARGV, 1)
|
83
|
+
local queues = ARGV
|
84
|
+
|
85
|
+
for _, queue in ipairs(queues) do
|
86
|
+
local busy_key = namespace..'busy:'..queue
|
87
|
+
redis.call('lrem', busy_key, 1, worker_name)
|
88
|
+
end
|
89
|
+
LUA
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Sidekiq::LimitFetch::Global
|
2
|
+
class Semaphore
|
3
|
+
PREFIX = 'limit_fetch'
|
4
|
+
|
5
|
+
def initialize(name)
|
6
|
+
@name = name
|
7
|
+
end
|
8
|
+
|
9
|
+
def limit
|
10
|
+
value = Sidekiq.redis {|it| it.get "#{PREFIX}:limit:#@name" }
|
11
|
+
value.to_i if value
|
12
|
+
end
|
13
|
+
|
14
|
+
def limit=(value)
|
15
|
+
Sidekiq.redis {|it| it.set "#{PREFIX}:limit:#@name", value }
|
16
|
+
end
|
17
|
+
|
18
|
+
def acquire
|
19
|
+
Selector.acquire([@name]).size > 0
|
20
|
+
end
|
21
|
+
|
22
|
+
def release
|
23
|
+
Selector.release [@name]
|
24
|
+
end
|
25
|
+
|
26
|
+
def busy
|
27
|
+
Sidekiq.redis {|it| it.llen "#{PREFIX}:busy:#@name" }
|
28
|
+
end
|
29
|
+
|
30
|
+
def pause
|
31
|
+
Sidekiq.redis {|it| it.set "#{PREFIX}:pause:#@name", true }
|
32
|
+
end
|
33
|
+
|
34
|
+
def continue
|
35
|
+
Sidekiq.redis {|it| it.del "#{PREFIX}:pause:#@name" }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Sidekiq::LimitFetch::Local
|
2
|
+
module Selector
|
3
|
+
extend self
|
4
|
+
|
5
|
+
def acquire(names)
|
6
|
+
queues(names).select(&:acquire).map(&:name)
|
7
|
+
end
|
8
|
+
|
9
|
+
def release(names)
|
10
|
+
queues(names).each(&:release)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def queues(names)
|
16
|
+
names.map {|name| Sidekiq::Queue[name] }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Sidekiq::LimitFetch::Local
|
2
|
+
class Semaphore
|
3
|
+
attr_reader :limit, :busy
|
4
|
+
|
5
|
+
def initialize(name)
|
6
|
+
@name = name
|
7
|
+
@lock = Mutex.new
|
8
|
+
@busy = 0
|
9
|
+
@paused = false
|
10
|
+
end
|
11
|
+
|
12
|
+
def limit=(value)
|
13
|
+
@lock.synchronize do
|
14
|
+
@limit = value
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def acquire
|
19
|
+
return if @paused
|
20
|
+
@lock.synchronize do
|
21
|
+
@busy += 1 if not @limit or @limit > @busy
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def release
|
26
|
+
@lock.synchronize do
|
27
|
+
@busy -= 1
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def pause
|
32
|
+
@paused = true
|
33
|
+
end
|
34
|
+
|
35
|
+
def continue
|
36
|
+
@paused = false
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
class Sidekiq::LimitFetch
|
2
|
+
class Queues
|
3
|
+
THREAD_KEY = :acquired_queues
|
4
|
+
attr_reader :selector
|
5
|
+
|
6
|
+
def initialize(options)
|
7
|
+
@queues = options[:queues]
|
8
|
+
options[:strict] ? strict_order! : weighted_order!
|
9
|
+
|
10
|
+
set_selector options[:global]
|
11
|
+
set_limits options[:limits]
|
12
|
+
end
|
13
|
+
|
14
|
+
def acquire
|
15
|
+
@selector.acquire(ordered_queues)
|
16
|
+
.tap {|it| save it }
|
17
|
+
.map {|it| "queue:#{it}" }
|
18
|
+
end
|
19
|
+
|
20
|
+
def release_except(full_name)
|
21
|
+
@selector.release restore.delete_if {|name| full_name.to_s.include? name }
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def set_selector(global)
|
27
|
+
@selector = global ? Global::Selector : Local::Selector
|
28
|
+
end
|
29
|
+
|
30
|
+
def set_limits(limits)
|
31
|
+
ordered_queues.each do |name|
|
32
|
+
Sidekiq::Queue[name].tap do |it|
|
33
|
+
it.limit = (limits || {})[name]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def strict_order!
|
39
|
+
@queues.uniq!
|
40
|
+
def ordered_queues; @queues end
|
41
|
+
end
|
42
|
+
|
43
|
+
def weighted_order!
|
44
|
+
def ordered_queues; @queues.shuffle.uniq end
|
45
|
+
end
|
46
|
+
|
47
|
+
def save(queues)
|
48
|
+
Thread.current[THREAD_KEY] = queues
|
49
|
+
end
|
50
|
+
|
51
|
+
def restore
|
52
|
+
Thread.current[THREAD_KEY]
|
53
|
+
ensure
|
54
|
+
Thread.current[THREAD_KEY] = nil
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -1,16 +1,12 @@
|
|
1
|
-
Sidekiq
|
2
|
-
|
1
|
+
module Sidekiq
|
2
|
+
class LimitFetch::UnitOfWork < BasicFetch::UnitOfWork
|
3
|
+
def acknowledge
|
4
|
+
Queue[queue_name].release
|
5
|
+
end
|
3
6
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
def acknowledge
|
9
|
-
release
|
10
|
-
end
|
11
|
-
|
12
|
-
def requeue
|
13
|
-
release
|
14
|
-
Sidekiq.redis {|it| it.rpush queue, message }
|
7
|
+
def requeue
|
8
|
+
super
|
9
|
+
acknowledge
|
10
|
+
end
|
15
11
|
end
|
16
12
|
end
|
data/sidekiq-limit_fetch.gemspec
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Gem::Specification.new do |gem|
|
2
2
|
gem.name = 'sidekiq-limit_fetch'
|
3
|
-
gem.version = '0.
|
3
|
+
gem.version = '0.6'
|
4
4
|
gem.authors = 'brainopia'
|
5
5
|
gem.email = 'brainopia@evilmartians.com'
|
6
6
|
gem.summary = 'Sidekiq strategy to support queue limits'
|
@@ -14,6 +14,6 @@ Gem::Specification.new do |gem|
|
|
14
14
|
gem.test_files = gem.files.grep %r{^spec/}
|
15
15
|
gem.require_paths = %w(lib)
|
16
16
|
|
17
|
-
gem.add_dependency 'sidekiq', '>= 2.6.
|
17
|
+
gem.add_dependency 'sidekiq', '>= 2.6.5'
|
18
18
|
gem.add_development_dependency 'rspec'
|
19
19
|
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
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
|
+
new_object.call.should == new_object.call
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'with given name' do
|
12
|
+
new_object = ->(name) { described_class.send constructor, name }
|
13
|
+
new_object.call('name').should == 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
|
+
shared_examples_for :lock do
|
32
|
+
it 'should be available' do
|
33
|
+
queue.acquire.should be
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'should be pausable' do
|
37
|
+
queue.pause
|
38
|
+
queue.acquire.should_not be
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'should be continuable' do
|
42
|
+
queue.pause
|
43
|
+
queue.continue
|
44
|
+
queue.acquire.should be
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'should be limitable' do
|
48
|
+
queue.limit = 1
|
49
|
+
queue.acquire.should be
|
50
|
+
queue.acquire.should_not be
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'should be resizable' do
|
54
|
+
queue.limit = 0
|
55
|
+
queue.acquire.should_not be
|
56
|
+
queue.limit = nil
|
57
|
+
queue.acquire.should be
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'should be countable' do
|
61
|
+
queue.limit = 3
|
62
|
+
5.times { queue.acquire }
|
63
|
+
queue.busy.should == 3
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'should be releasable' do
|
67
|
+
queue.acquire
|
68
|
+
queue.busy.should == 1
|
69
|
+
queue.release
|
70
|
+
queue.busy.should == 0
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
context 'global' do
|
75
|
+
before(:all) { Sidekiq.options[:global] = true }
|
76
|
+
it_behaves_like :lock
|
77
|
+
end
|
78
|
+
|
79
|
+
context 'local' do
|
80
|
+
before(:all) { Sidekiq.options[:global] = false }
|
81
|
+
it_behaves_like :lock
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Sidekiq::LimitFetch::Queues do
|
4
|
+
subject { described_class.new options }
|
5
|
+
|
6
|
+
let(:queues) { %w[queue1 queue2] }
|
7
|
+
let(:limits) {{ 'queue1' => 3 }}
|
8
|
+
let(:strict) { true }
|
9
|
+
let(:global) { false }
|
10
|
+
|
11
|
+
let(:options) do
|
12
|
+
{ queues: queues, limits: limits, strict: strict, global: global }
|
13
|
+
end
|
14
|
+
|
15
|
+
after(:each ) do
|
16
|
+
Thread.current[:available_queues] = nil
|
17
|
+
end
|
18
|
+
|
19
|
+
shared_examples_for :selector do
|
20
|
+
it 'should acquire queues' do
|
21
|
+
subject.acquire
|
22
|
+
Sidekiq::Queue['queue1'].busy.should == 1
|
23
|
+
Sidekiq::Queue['queue2'].busy.should == 1
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'should release queues' do
|
27
|
+
subject.acquire
|
28
|
+
subject.release_except nil
|
29
|
+
Sidekiq::Queue['queue1'].busy.should == 0
|
30
|
+
Sidekiq::Queue['queue2'].busy.should == 0
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'should release queues except selected' do
|
34
|
+
subject.acquire
|
35
|
+
subject.release_except 'queue:queue1'
|
36
|
+
Sidekiq::Queue['queue1'].busy.should == 1
|
37
|
+
Sidekiq::Queue['queue2'].busy.should == 0
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
context 'without global flag' do
|
42
|
+
it_should_behave_like :selector
|
43
|
+
|
44
|
+
it 'without global flag should be local' do
|
45
|
+
subject.selector.should == Sidekiq::LimitFetch::Local::Selector
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
context 'with global flag' do
|
50
|
+
let(:global) { true }
|
51
|
+
it_should_behave_like :selector
|
52
|
+
|
53
|
+
it 'should use global selector' do
|
54
|
+
subject.selector.should == Sidekiq::LimitFetch::Global::Selector
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'should set limits' do
|
59
|
+
subject
|
60
|
+
Sidekiq::Queue['queue1'].limit.should == 3
|
61
|
+
Sidekiq::Queue['queue2'].limit.should_not be
|
62
|
+
end
|
63
|
+
|
64
|
+
context 'without strict flag' do
|
65
|
+
let(:strict) { false }
|
66
|
+
|
67
|
+
it 'should retrieve weighted queues' do
|
68
|
+
subject.ordered_queues.should =~ %w(queue1 queue2)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'with strict flag should retrieve strictly ordered queues' do
|
73
|
+
subject.ordered_queues.should == %w(queue1 queue2)
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'semaphore' do
|
4
|
+
shared_examples_for :semaphore do
|
5
|
+
it 'should have no limit by default' do
|
6
|
+
subject.limit.should_not be
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'should set limit' do
|
10
|
+
subject.limit = 4
|
11
|
+
subject.limit.should == 4
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'should acquire and count active tasks' do
|
15
|
+
3.times { subject.acquire }
|
16
|
+
subject.busy.should == 3
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'should acquire tasks with regard to limit' do
|
20
|
+
subject.limit = 4
|
21
|
+
6.times { subject.acquire }
|
22
|
+
subject.busy.should == 4
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'should release active tasks' do
|
26
|
+
6.times { subject.acquire }
|
27
|
+
3.times { subject.release }
|
28
|
+
subject.busy.should == 3
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'should pause tasks' do
|
32
|
+
3.times { subject.acquire }
|
33
|
+
subject.pause
|
34
|
+
2.times { subject.acquire }
|
35
|
+
subject.busy.should == 3
|
36
|
+
2.times { subject.release }
|
37
|
+
subject.busy.should == 1
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'should unpause tasks' do
|
41
|
+
subject.pause
|
42
|
+
3.times { subject.acquire }
|
43
|
+
subject.continue
|
44
|
+
2.times { subject.acquire }
|
45
|
+
subject.busy.should == 2
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
let(:name) { 'default' }
|
50
|
+
|
51
|
+
context 'local' do
|
52
|
+
subject { Sidekiq::LimitFetch::Local::Semaphore.new name }
|
53
|
+
it_behaves_like :semaphore
|
54
|
+
end
|
55
|
+
|
56
|
+
context 'global' do
|
57
|
+
subject { Sidekiq::LimitFetch::Global::Semaphore.new name }
|
58
|
+
it_behaves_like :semaphore
|
59
|
+
|
60
|
+
after :each do
|
61
|
+
Sidekiq.redis do |it|
|
62
|
+
it.del "limit_fetch:limit:#{name}"
|
63
|
+
it.del "limit_fetch:busy:#{name}"
|
64
|
+
it.del "limit_fetch:pause:#{name}"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Sidekiq::LimitFetch do
|
4
|
+
before :each do
|
5
|
+
Sidekiq.redis do |it|
|
6
|
+
it.del 'queue:queue1'
|
7
|
+
it.rpush 'queue:queue1', 'task1'
|
8
|
+
it.rpush 'queue:queue1', 'task2'
|
9
|
+
it.expire 'queue:queue1', 30
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
subject { described_class.new options }
|
14
|
+
let(:options) {{ queues: queues, limits: limits, global: global }}
|
15
|
+
let(:queues) { %w(queue1 queue1 queue2 queue2) }
|
16
|
+
let(:limits) {{ 'queue1' => 1, 'queue2' => 2 }}
|
17
|
+
|
18
|
+
shared_examples_for :strategy do
|
19
|
+
it 'should acquire lock on queue for execution' do
|
20
|
+
work = subject.retrieve_work
|
21
|
+
work.queue_name.should == 'queue1'
|
22
|
+
work.message.should == 'task1'
|
23
|
+
|
24
|
+
subject.retrieve_work.should_not be
|
25
|
+
work.requeue
|
26
|
+
|
27
|
+
work = subject.retrieve_work
|
28
|
+
work.message.should == 'task2'
|
29
|
+
|
30
|
+
subject.retrieve_work.should_not be
|
31
|
+
work.acknowledge
|
32
|
+
|
33
|
+
work = subject.retrieve_work
|
34
|
+
work.message.should == 'task1'
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
context 'global' do
|
39
|
+
let(:global) { true }
|
40
|
+
it_behaves_like :strategy
|
41
|
+
end
|
42
|
+
|
43
|
+
context 'local' do
|
44
|
+
let(:global) { false }
|
45
|
+
it_behaves_like :strategy
|
46
|
+
end
|
47
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -3,5 +3,17 @@ require 'sidekiq/limit_fetch'
|
|
3
3
|
RSpec.configure do |config|
|
4
4
|
config.before :each do
|
5
5
|
Sidekiq::Queue.instance_variable_set :@instances, {}
|
6
|
+
Sidekiq.options[:global] = defined?(global) ? global : nil
|
7
|
+
|
8
|
+
Sidekiq.redis do |it|
|
9
|
+
clean_redis = ->(queue) do
|
10
|
+
it.del "limit_fetch:limit:#{queue}"
|
11
|
+
it.del "limit_fetch:busy:#{queue}"
|
12
|
+
it.del "limit_fetch:pause:#{queue}"
|
13
|
+
end
|
14
|
+
|
15
|
+
clean_redis.call(name) if defined?(name)
|
16
|
+
queues.each(&clean_redis) if defined?(queues) and queues.is_a? Array
|
17
|
+
end
|
6
18
|
end
|
7
19
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sidekiq-limit_fetch
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: '0.
|
4
|
+
version: '0.6'
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,40 +9,40 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-01-
|
12
|
+
date: 2013-01-24 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: sidekiq
|
16
|
-
prerelease: false
|
17
16
|
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
20
20
|
- !ruby/object:Gem::Version
|
21
|
-
version: 2.6.
|
22
|
-
none: false
|
21
|
+
version: 2.6.5
|
23
22
|
type: :runtime
|
23
|
+
prerelease: false
|
24
24
|
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
25
26
|
requirements:
|
26
27
|
- - ! '>='
|
27
28
|
- !ruby/object:Gem::Version
|
28
|
-
version: 2.6.
|
29
|
-
none: false
|
29
|
+
version: 2.6.5
|
30
30
|
- !ruby/object:Gem::Dependency
|
31
31
|
name: rspec
|
32
|
-
prerelease: false
|
33
32
|
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
34
|
requirements:
|
35
35
|
- - ! '>='
|
36
36
|
- !ruby/object:Gem::Version
|
37
37
|
version: '0'
|
38
|
-
none: false
|
39
38
|
type: :development
|
39
|
+
prerelease: false
|
40
40
|
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
41
42
|
requirements:
|
42
43
|
- - ! '>='
|
43
44
|
- !ruby/object:Gem::Version
|
44
45
|
version: '0'
|
45
|
-
none: false
|
46
46
|
description: ! " Sidekiq strategy to restrict number of workers\n which are
|
47
47
|
able to run specified queues simultaneously.\n"
|
48
48
|
email: brainopia@evilmartians.com
|
@@ -55,14 +55,20 @@ files:
|
|
55
55
|
- LICENSE.txt
|
56
56
|
- README.md
|
57
57
|
- Rakefile
|
58
|
+
- lib/sidekiq/extensions/queue.rb
|
58
59
|
- lib/sidekiq/limit_fetch.rb
|
59
|
-
- lib/sidekiq/limit_fetch/
|
60
|
-
- lib/sidekiq/limit_fetch/semaphore.rb
|
60
|
+
- lib/sidekiq/limit_fetch/global/selector.rb
|
61
|
+
- lib/sidekiq/limit_fetch/global/semaphore.rb
|
62
|
+
- lib/sidekiq/limit_fetch/local/selector.rb
|
63
|
+
- lib/sidekiq/limit_fetch/local/semaphore.rb
|
64
|
+
- lib/sidekiq/limit_fetch/queues.rb
|
61
65
|
- lib/sidekiq/limit_fetch/singleton.rb
|
62
66
|
- lib/sidekiq/limit_fetch/unit_of_work.rb
|
63
67
|
- sidekiq-limit_fetch.gemspec
|
64
|
-
- spec/
|
65
|
-
- spec/sidekiq/limit_fetch/
|
68
|
+
- spec/sidekiq/extensions/queue_spec.rb
|
69
|
+
- spec/sidekiq/limit_fetch/queues_spec.rb
|
70
|
+
- spec/sidekiq/limit_fetch/semaphore_spec.rb
|
71
|
+
- spec/sidekiq/limit_fetch_spec.rb
|
66
72
|
- spec/spec_helper.rb
|
67
73
|
homepage: https://github.com/brainopia/sidekiq-limit_fetch
|
68
74
|
licenses: []
|
@@ -71,17 +77,17 @@ rdoc_options: []
|
|
71
77
|
require_paths:
|
72
78
|
- lib
|
73
79
|
required_ruby_version: !ruby/object:Gem::Requirement
|
80
|
+
none: false
|
74
81
|
requirements:
|
75
82
|
- - ! '>='
|
76
83
|
- !ruby/object:Gem::Version
|
77
84
|
version: '0'
|
78
|
-
none: false
|
79
85
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
86
|
+
none: false
|
80
87
|
requirements:
|
81
88
|
- - ! '>='
|
82
89
|
- !ruby/object:Gem::Version
|
83
90
|
version: '0'
|
84
|
-
none: false
|
85
91
|
requirements: []
|
86
92
|
rubyforge_project:
|
87
93
|
rubygems_version: 1.8.24
|
@@ -89,6 +95,8 @@ signing_key:
|
|
89
95
|
specification_version: 3
|
90
96
|
summary: Sidekiq strategy to support queue limits
|
91
97
|
test_files:
|
92
|
-
- spec/
|
93
|
-
- spec/sidekiq/limit_fetch/
|
98
|
+
- spec/sidekiq/extensions/queue_spec.rb
|
99
|
+
- spec/sidekiq/limit_fetch/queues_spec.rb
|
100
|
+
- spec/sidekiq/limit_fetch/semaphore_spec.rb
|
101
|
+
- spec/sidekiq/limit_fetch_spec.rb
|
94
102
|
- spec/spec_helper.rb
|
@@ -1,26 +0,0 @@
|
|
1
|
-
class Sidekiq::LimitFetch::Semaphore
|
2
|
-
attr_reader :limit, :busy
|
3
|
-
|
4
|
-
def initialize
|
5
|
-
@lock = Mutex.new
|
6
|
-
@busy = 0
|
7
|
-
end
|
8
|
-
|
9
|
-
def limit=(value)
|
10
|
-
@lock.synchronize do
|
11
|
-
@limit = value
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
|
-
def acquire
|
16
|
-
@lock.synchronize do
|
17
|
-
@busy += 1 if not @limit or @limit > @busy
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
def release
|
22
|
-
@lock.synchronize do
|
23
|
-
@busy -= 1
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
data/spec/limit_fetch_spec.rb
DELETED
@@ -1,81 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
describe Sidekiq::LimitFetch do
|
4
|
-
before :each do
|
5
|
-
Sidekiq.redis do |it|
|
6
|
-
it.del 'queue:example1'
|
7
|
-
it.rpush 'queue:example1', 'task'
|
8
|
-
it.expire 'queue:example1', 30
|
9
|
-
end
|
10
|
-
end
|
11
|
-
|
12
|
-
def queues(fetcher)
|
13
|
-
fetcher.available_queues.map(&:full_name)
|
14
|
-
end
|
15
|
-
|
16
|
-
def new_fetcher(options={})
|
17
|
-
described_class.new options.merge queues: %w(example1 example1 example2 example2)
|
18
|
-
end
|
19
|
-
|
20
|
-
it 'should retrieve weighted queues' do
|
21
|
-
fetcher = new_fetcher
|
22
|
-
queues(fetcher).should =~ %w(queue:example1 queue:example2)
|
23
|
-
end
|
24
|
-
|
25
|
-
it 'should retrieve strictly ordered queues' do
|
26
|
-
fetcher = new_fetcher strict: true
|
27
|
-
queues(fetcher).should == %w(queue:example1 queue:example2)
|
28
|
-
end
|
29
|
-
|
30
|
-
it 'should retrieve only available queues' do
|
31
|
-
fetcher = new_fetcher strict: true, limits: { 'example1' => 2 }
|
32
|
-
queues = -> { fetcher.available_queues }
|
33
|
-
|
34
|
-
queues1 = queues.call
|
35
|
-
queues2 = queues.call
|
36
|
-
queues1.should have(2).items
|
37
|
-
queues2.should have(2).items
|
38
|
-
queues.call.should have(1).items
|
39
|
-
|
40
|
-
queues1.each(&:release)
|
41
|
-
queues.call.should have(2).items
|
42
|
-
queues.call.should have(1).items
|
43
|
-
|
44
|
-
queues2.each(&:release)
|
45
|
-
queues.call.should have(2).items
|
46
|
-
queues.call.should have(1).items
|
47
|
-
end
|
48
|
-
|
49
|
-
it 'should acquire lock on queue for excecution' do
|
50
|
-
fetcher = new_fetcher limits: { 'example1' => 1, 'example2' => 1 }
|
51
|
-
work = fetcher.retrieve_work
|
52
|
-
work.message.should == 'task'
|
53
|
-
work.queue.should == 'queue:example1'
|
54
|
-
work.queue_name.should == 'example1'
|
55
|
-
|
56
|
-
queues = fetcher.available_queues
|
57
|
-
queues.should have(1).item
|
58
|
-
queues.each(&:release)
|
59
|
-
|
60
|
-
work.requeue
|
61
|
-
work = fetcher.retrieve_work
|
62
|
-
work.message.should == 'task'
|
63
|
-
work.acknowledge
|
64
|
-
|
65
|
-
fetcher.available_queues.should have(2).items
|
66
|
-
end
|
67
|
-
|
68
|
-
it 'should set queue limits on the fly' do
|
69
|
-
Sidekiq::Queue['example1'].limit = 1
|
70
|
-
Sidekiq::Queue['example2'].limit = 2
|
71
|
-
|
72
|
-
fetcher = new_fetcher
|
73
|
-
|
74
|
-
fetcher.available_queues.should have(2).item
|
75
|
-
fetcher.available_queues.should have(1).item
|
76
|
-
fetcher.available_queues.should have(0).item
|
77
|
-
|
78
|
-
Sidekiq::Queue['example1'].limit = 2
|
79
|
-
fetcher.available_queues.should have(1).item
|
80
|
-
end
|
81
|
-
end
|
@@ -1,27 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
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
|
-
new_object.call.should == new_object.call
|
9
|
-
end
|
10
|
-
|
11
|
-
it 'with given name' do
|
12
|
-
new_object = ->(name) { described_class.send constructor, name }
|
13
|
-
new_object.call('name').should == 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
|
-
end
|
27
|
-
end
|