throttle-queue 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/LICENSE.txt +1 -0
- data/README.md +48 -0
- data/Rakefile +1 -0
- data/lib/throttle-queue.rb +1 -222
- data/lib/throttle-queue/multi-process.rb +103 -0
- data/lib/throttle-queue/single-process.rb +224 -0
- data/test/multiprocess-test.rb +88 -0
- data/test/throttle-queue-test.rb +28 -2
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0d0f4a538c9d23d2a21d79b63122c4a0074e4095
|
4
|
+
data.tar.gz: 16859c8f40a47777526ec299ed4721ba955ca55f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a39194f269635e9b8ece84714010df1e1cedd0194c5133a77fdfb277d71227aa4bf88e1d57e4b5fba2c84e470831a04b5ede2d8f8f287dd83baac54154f29229
|
7
|
+
data.tar.gz: dc984657a36e8238294813d3f10e36ae837bd77764ee0c6a8c2be81648cedaeb2dda1a5334658a75ffab4cf01435ab6e02fe1f24284b73cb74c9029345c7b0ba
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -18,10 +18,24 @@ Or install it yourself as:
|
|
18
18
|
|
19
19
|
$ gem install throttle-queue
|
20
20
|
|
21
|
+
## Purpose
|
22
|
+
|
23
|
+
Sometimes server resources are rate-limited. If you as a client exceed the server's
|
24
|
+
limit, you become temporarily black-listed. A popular set of free APIs, which rhymes
|
25
|
+
with Foogle Maps, has strict limits in place after which you start seeing 403s instead
|
26
|
+
of 200s.
|
27
|
+
|
28
|
+
Let's say, for the above reason or another, you want to accumulate a cache of objects
|
29
|
+
no faster than N times a second. You're happy to startup your app quickly and let your
|
30
|
+
cache grow in the background. When a user requests a resource, you can bump it to the
|
31
|
+
front of your queue and block until it is ready.
|
32
|
+
|
21
33
|
## Usage
|
22
34
|
|
23
35
|
Create a queue and add background work
|
24
36
|
|
37
|
+
require 'throttle-queue'
|
38
|
+
|
25
39
|
q = ThrottleQueue.new 3
|
26
40
|
files.each {|file|
|
27
41
|
q.background(file) {
|
@@ -39,6 +53,39 @@ Wait for everything to finish
|
|
39
53
|
|
40
54
|
q.wait
|
41
55
|
|
56
|
+
## Details
|
57
|
+
|
58
|
+
Each resource is assumed to have a unique identifier, e.g. a filename or a reproducible
|
59
|
+
hash value. The queue does not check if the resource exists first, but it will check if
|
60
|
+
the id has already been queued. Any time an id is added to the queue, the previous block
|
61
|
+
is dropped in favor of the new.
|
62
|
+
|
63
|
+
Once an id has made it through the queue and been processed, the same id can be added
|
64
|
+
again and will be blindly processed again. It is assumed the user of the object knows
|
65
|
+
whether the resource already exists, and will decide whether or not a given id should be
|
66
|
+
added.
|
67
|
+
|
68
|
+
## Multiple Processes
|
69
|
+
|
70
|
+
Just bring in the multi-process-aware queue wrapper
|
71
|
+
|
72
|
+
require 'throttle-queue/multi-process'
|
73
|
+
|
74
|
+
q = ThrottleQueue::MultiProcess.new 3
|
75
|
+
files.each {|file|
|
76
|
+
q.background(file) {
|
77
|
+
fetch file
|
78
|
+
}
|
79
|
+
}
|
80
|
+
|
81
|
+
Use this queue the same way in each process. Make sure each queue id has the same
|
82
|
+
meaning in each process (e.g. file paths should be absolute or relative to the same
|
83
|
+
working directory). If your processes race to stack up a bunch of background work, the
|
84
|
+
first process to add each work will be the only one to execute its block.
|
85
|
+
|
86
|
+
Do not fork after a multi-process queue is created. If your process forks itself,
|
87
|
+
create each queue after the fork.
|
88
|
+
|
42
89
|
## Contributing
|
43
90
|
|
44
91
|
1. Fork it
|
@@ -46,3 +93,4 @@ Wait for everything to finish
|
|
46
93
|
3. Commit your changes (`git commit -am 'Add some feature'`)
|
47
94
|
4. Push to the branch (`git push origin my-new-feature`)
|
48
95
|
5. Create new Pull Request
|
96
|
+
|
data/Rakefile
CHANGED
data/lib/throttle-queue.rb
CHANGED
@@ -1,222 +1 @@
|
|
1
|
-
|
2
|
-
# ThrottleQueue is a thread-safe rate-limited work queue. It allows both
|
3
|
-
# background and foreground operations.
|
4
|
-
#
|
5
|
-
# Example:
|
6
|
-
# q = ThrottleQueue 3
|
7
|
-
# files.each {|file|
|
8
|
-
# q.background(file) {|id|
|
9
|
-
# fetch file
|
10
|
-
# }
|
11
|
-
# }
|
12
|
-
class ThrottleQueue
|
13
|
-
# Creates a new ThrottleQueue with the given rate limit (per second).
|
14
|
-
def initialize(limit)
|
15
|
-
raise "refusing to do zero work per second" if limit <= 0
|
16
|
-
@limit = limit
|
17
|
-
|
18
|
-
@queue = PriorityQueue.new
|
19
|
-
|
20
|
-
@mutex = Mutex.new
|
21
|
-
@pausing = ConditionVariable.new
|
22
|
-
@idle = ConditionVariable.new
|
23
|
-
@in_flight = nil
|
24
|
-
@processing_thread = nil
|
25
|
-
@items = {}
|
26
|
-
|
27
|
-
@throttling = nil
|
28
|
-
@state = :idle
|
29
|
-
@t0 = Time.now
|
30
|
-
end
|
31
|
-
# Signals the queue to stop processing and shutdown.
|
32
|
-
#
|
33
|
-
# Items still in the queue are dropped. Any item
|
34
|
-
# currently in flight will finish.
|
35
|
-
def shutdown
|
36
|
-
@queue.shutdown
|
37
|
-
@pausing.signal
|
38
|
-
end
|
39
|
-
# Returns true if there is nothing queued and no
|
40
|
-
# threads are running
|
41
|
-
def idle?
|
42
|
-
@state == :idle
|
43
|
-
end
|
44
|
-
# Blocks the calling thread while the queue processes work.
|
45
|
-
#
|
46
|
-
# Returns after the timeout has expired, or after the
|
47
|
-
# queue returns to the idle state.
|
48
|
-
def wait(timeout = nil)
|
49
|
-
@mutex.synchronize {
|
50
|
-
@idle.wait(@mutex, timeout) unless idle?
|
51
|
-
}
|
52
|
-
end
|
53
|
-
# Adds work to the queue to run in the background, and
|
54
|
-
# returns immediately.
|
55
|
-
#
|
56
|
-
# If the block takes an argument, it will be passed the
|
57
|
-
# same id used to queue the work.
|
58
|
-
def background(id, &block)
|
59
|
-
@mutex.synchronize {
|
60
|
-
if id != @in_flight
|
61
|
-
@items[id] = block
|
62
|
-
@queue.background id
|
63
|
-
run
|
64
|
-
end
|
65
|
-
}
|
66
|
-
end
|
67
|
-
# Adds work to the queue ahead of all background work, and
|
68
|
-
# blocks until the given block has been called.
|
69
|
-
#
|
70
|
-
# Will preempt an id of the same value in either the
|
71
|
-
# background or foreground queues.
|
72
|
-
#
|
73
|
-
# If the block takes an argument, it will be passed the
|
74
|
-
# same id used to queue the work.
|
75
|
-
def foreground(id, &block)
|
76
|
-
t = nil
|
77
|
-
@mutex.synchronize {
|
78
|
-
if id == @in_flight
|
79
|
-
t = @processing_thread unless @processing_thread == Thread.current
|
80
|
-
else
|
81
|
-
b = @items[id]
|
82
|
-
b.kill if b.is_a? FG
|
83
|
-
|
84
|
-
t = @items[id] = FG.new block, self
|
85
|
-
|
86
|
-
@queue.foreground id
|
87
|
-
run
|
88
|
-
end
|
89
|
-
}
|
90
|
-
t.join if t
|
91
|
-
end
|
92
|
-
|
93
|
-
private
|
94
|
-
def run
|
95
|
-
return unless @state == :idle
|
96
|
-
@state = :running
|
97
|
-
@throttling = Thread.new {
|
98
|
-
loop {
|
99
|
-
break if @queue.shutdown? or @queue.empty?
|
100
|
-
|
101
|
-
elapsed = Time.now - @t0
|
102
|
-
wait_time = 1.0 / @limit + 0.01
|
103
|
-
if @processing_thread and elapsed < wait_time
|
104
|
-
@mutex.synchronize {
|
105
|
-
@pausing.wait @mutex, wait_time - elapsed
|
106
|
-
}
|
107
|
-
end
|
108
|
-
|
109
|
-
if id = @queue.pop
|
110
|
-
@mutex.synchronize {
|
111
|
-
@in_flight = id
|
112
|
-
@processing_thread = Thread.new {
|
113
|
-
block = @items[@in_flight]
|
114
|
-
if block.arity == 0
|
115
|
-
block.call
|
116
|
-
else
|
117
|
-
block.call @in_flight
|
118
|
-
end
|
119
|
-
}
|
120
|
-
}
|
121
|
-
@processing_thread.join if @processing_thread
|
122
|
-
end
|
123
|
-
|
124
|
-
@t0 = Time.now
|
125
|
-
}
|
126
|
-
|
127
|
-
@mutex.synchronize {
|
128
|
-
@state = :idle
|
129
|
-
if @queue.shutdown? or @queue.empty?
|
130
|
-
@idle.signal
|
131
|
-
else
|
132
|
-
# Restart to prevent a join deadlock
|
133
|
-
send :run
|
134
|
-
end
|
135
|
-
}
|
136
|
-
}
|
137
|
-
end
|
138
|
-
class FG #:nodoc: all
|
139
|
-
def initialize(block, h)
|
140
|
-
@block = block
|
141
|
-
@thread = Thread.new {
|
142
|
-
Thread.stop
|
143
|
-
@block.call *@args
|
144
|
-
}
|
145
|
-
@h = h
|
146
|
-
end
|
147
|
-
def arity
|
148
|
-
@block.arity
|
149
|
-
end
|
150
|
-
def call(*args)
|
151
|
-
@args = args
|
152
|
-
@thread.run
|
153
|
-
end
|
154
|
-
def kill
|
155
|
-
@thread.kill
|
156
|
-
end
|
157
|
-
def join
|
158
|
-
@thread.join
|
159
|
-
end
|
160
|
-
end
|
161
|
-
class PriorityQueue #:nodoc: all
|
162
|
-
def initialize
|
163
|
-
@mutex = Mutex.new
|
164
|
-
@fg = []
|
165
|
-
@bg = []
|
166
|
-
@received = ConditionVariable.new
|
167
|
-
@shutdown = false
|
168
|
-
end
|
169
|
-
|
170
|
-
def shutdown
|
171
|
-
@shutdown = true
|
172
|
-
@received.signal
|
173
|
-
end
|
174
|
-
|
175
|
-
def shutdown?
|
176
|
-
@shutdown
|
177
|
-
end
|
178
|
-
|
179
|
-
def empty?
|
180
|
-
@mutex.synchronize {
|
181
|
-
@fg.empty? and @bg.empty?
|
182
|
-
}
|
183
|
-
end
|
184
|
-
|
185
|
-
def background(id)
|
186
|
-
@mutex.synchronize {
|
187
|
-
unless @shutdown || @bg.include?(id)
|
188
|
-
@bg << id
|
189
|
-
@received.signal
|
190
|
-
end
|
191
|
-
}
|
192
|
-
end
|
193
|
-
|
194
|
-
def foreground(id)
|
195
|
-
@mutex.synchronize {
|
196
|
-
unless @shutdown || @fg.include?(id)
|
197
|
-
@fg << id
|
198
|
-
if @bg.include?(id)
|
199
|
-
@bg.delete id
|
200
|
-
else
|
201
|
-
@received.signal
|
202
|
-
end
|
203
|
-
end
|
204
|
-
}
|
205
|
-
end
|
206
|
-
|
207
|
-
def pop
|
208
|
-
@mutex.synchronize {
|
209
|
-
if @fg.empty? and @bg.empty?
|
210
|
-
@received.wait(@mutex) unless @shutdown
|
211
|
-
end
|
212
|
-
|
213
|
-
if @shutdown
|
214
|
-
elsif ! @fg.empty?
|
215
|
-
@fg.shift
|
216
|
-
else
|
217
|
-
@bg.shift
|
218
|
-
end
|
219
|
-
}
|
220
|
-
end
|
221
|
-
end
|
222
|
-
end
|
1
|
+
require_relative 'throttle-queue/single-process'
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'drb'
|
2
|
+
require 'fileutils'
|
3
|
+
require_relative 'single-process.rb'
|
4
|
+
|
5
|
+
class ThrottleQueue
|
6
|
+
# ThrottleQueue::MultiProcess is a wrapper around ThrottleQueue
|
7
|
+
# that shares the queue between multiple processes.
|
8
|
+
#
|
9
|
+
# Example:
|
10
|
+
# q = ThrottleQueue::MultiProcess 3
|
11
|
+
# files.each {|file|
|
12
|
+
# q.background(file) {|id|
|
13
|
+
# fetch file
|
14
|
+
# }
|
15
|
+
# }
|
16
|
+
class MultiProcess
|
17
|
+
# Creates a new ThrottleQueue::MultiProcess with the given rate limit (per second).
|
18
|
+
#
|
19
|
+
# If this is the first instace of the shared queue, it becomes the master queue and
|
20
|
+
# starts a DRbServer instace. If a DRbServer is already running, it connects to the
|
21
|
+
# queue as a remote DRbObject.
|
22
|
+
def initialize(limit, name = 'ThrottleQueue')
|
23
|
+
tmp = "/tmp/#{name}.sock"
|
24
|
+
FileUtils.touch tmp
|
25
|
+
File.open(tmp, 'r+') {|f|
|
26
|
+
f.flock File::LOCK_EX
|
27
|
+
begin
|
28
|
+
port = f.read.to_i
|
29
|
+
if port == 0
|
30
|
+
@queue = ThrottleQueue.new(limit)
|
31
|
+
@drb = DRb.start_service nil, @queue
|
32
|
+
f.seek 0, IO::SEEK_SET
|
33
|
+
f.truncate 0
|
34
|
+
f.write @drb.uri[/\d+$/]
|
35
|
+
f.flock File::LOCK_UN
|
36
|
+
else
|
37
|
+
@queue = DRbObject.new_with_uri("druby://localhost:#{port}")
|
38
|
+
@queue.idle?
|
39
|
+
@drb = DRb.start_service
|
40
|
+
f.flock File::LOCK_UN
|
41
|
+
end
|
42
|
+
rescue DRb::DRbConnError
|
43
|
+
f.seek 0, IO::SEEK_SET
|
44
|
+
f.truncate 0
|
45
|
+
retry
|
46
|
+
end
|
47
|
+
}
|
48
|
+
|
49
|
+
end
|
50
|
+
# Signals the queue to stop processing and shutdown.
|
51
|
+
#
|
52
|
+
# The DRbServer is shutdown in either the master process or any
|
53
|
+
# client process.
|
54
|
+
def shutdown
|
55
|
+
@queue.shutdown
|
56
|
+
@drb.stop_service if @drb
|
57
|
+
end
|
58
|
+
# Returns true if there is nothing queued and no
|
59
|
+
# threads are running
|
60
|
+
def idle?
|
61
|
+
@queue.idle?
|
62
|
+
end
|
63
|
+
# Blocks the calling thread while the queue processes work.
|
64
|
+
#
|
65
|
+
# Returns after the timeout has expired, or after the
|
66
|
+
# queue returns to the idle state.
|
67
|
+
def wait(timeout = nil)
|
68
|
+
begin
|
69
|
+
@queue.wait(timeout)
|
70
|
+
rescue DRb::DRbConnError
|
71
|
+
end
|
72
|
+
end
|
73
|
+
# Adds work to the queue to run in the background, and
|
74
|
+
# returns immediately.
|
75
|
+
#
|
76
|
+
# If the block takes an argument, it will be passed the
|
77
|
+
# same id used to queue the work.
|
78
|
+
#
|
79
|
+
# The block may be preempted by a foreground job started in
|
80
|
+
# this or another process. If not preempted, the block will
|
81
|
+
# run in this process.
|
82
|
+
def background(id, &block)
|
83
|
+
@queue.background(id, &block)
|
84
|
+
end
|
85
|
+
# Adds work to the queue ahead of all background work, and
|
86
|
+
# blocks until the given block has been called.
|
87
|
+
#
|
88
|
+
# Will preempt an id of the same value in the
|
89
|
+
# background queue, and wait on an id of the same value already
|
90
|
+
# in the foreground queue.
|
91
|
+
#
|
92
|
+
# If the block takes an argument, it will be passed the
|
93
|
+
# same id used to queue the work.
|
94
|
+
#
|
95
|
+
# The block may wait on an already queued foreground job in
|
96
|
+
# this or another process. If so queued, this block will not
|
97
|
+
# run. If the block does run, it will run in this process.
|
98
|
+
def foreground(id, &block)
|
99
|
+
@queue.foreground(id, &block)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
@@ -0,0 +1,224 @@
|
|
1
|
+
require 'thread'
|
2
|
+
# ThrottleQueue is a thread-safe rate-limited work queue. It allows both
|
3
|
+
# background and foreground operations.
|
4
|
+
#
|
5
|
+
# Example:
|
6
|
+
# q = ThrottleQueue 3
|
7
|
+
# files.each {|file|
|
8
|
+
# q.background(file) {|id|
|
9
|
+
# fetch file
|
10
|
+
# }
|
11
|
+
# }
|
12
|
+
class ThrottleQueue
|
13
|
+
# Creates a new ThrottleQueue with the given rate limit (per second).
|
14
|
+
def initialize(limit)
|
15
|
+
raise "refusing to do zero work per second" if limit <= 0
|
16
|
+
@limit = limit
|
17
|
+
|
18
|
+
@queue = PriorityQueue.new
|
19
|
+
|
20
|
+
@mutex = Mutex.new
|
21
|
+
@pausing = ConditionVariable.new
|
22
|
+
@idle = ConditionVariable.new
|
23
|
+
@in_flight = nil
|
24
|
+
@processing_thread = nil
|
25
|
+
@items = {}
|
26
|
+
|
27
|
+
@throttling = nil
|
28
|
+
@state = :idle
|
29
|
+
@t0 = Time.now
|
30
|
+
end
|
31
|
+
# Signals the queue to stop processing and shutdown.
|
32
|
+
#
|
33
|
+
# Items still in the queue are dropped. Any item
|
34
|
+
# currently in flight will finish.
|
35
|
+
def shutdown
|
36
|
+
@queue.shutdown
|
37
|
+
@pausing.signal
|
38
|
+
end
|
39
|
+
# Returns true if there is nothing queued and no
|
40
|
+
# threads are running
|
41
|
+
def idle?
|
42
|
+
@state == :idle
|
43
|
+
end
|
44
|
+
# Blocks the calling thread while the queue processes work.
|
45
|
+
#
|
46
|
+
# Returns after the timeout has expired, or after the
|
47
|
+
# queue returns to the idle state.
|
48
|
+
def wait(timeout = nil)
|
49
|
+
@mutex.synchronize {
|
50
|
+
@idle.wait(@mutex, timeout) unless idle?
|
51
|
+
}
|
52
|
+
end
|
53
|
+
# Adds work to the queue to run in the background, and
|
54
|
+
# returns immediately.
|
55
|
+
#
|
56
|
+
# If the block takes an argument, it will be passed the
|
57
|
+
# same id used to queue the work.
|
58
|
+
def background(id, &block)
|
59
|
+
@mutex.synchronize {
|
60
|
+
unless @items.has_key? id
|
61
|
+
@items[id] = block
|
62
|
+
@queue.background id
|
63
|
+
run
|
64
|
+
end
|
65
|
+
}
|
66
|
+
end
|
67
|
+
# Adds work to the queue ahead of all background work, and
|
68
|
+
# blocks until the given block has been called.
|
69
|
+
#
|
70
|
+
# Will preempt an id of the same value in the
|
71
|
+
# background queue, and wait on an id of the same value already
|
72
|
+
# in the foreground queue.
|
73
|
+
#
|
74
|
+
# If the block takes an argument, it will be passed the
|
75
|
+
# same id used to queue the work.
|
76
|
+
def foreground(id, &block)
|
77
|
+
t = nil
|
78
|
+
@mutex.synchronize {
|
79
|
+
if id == @in_flight
|
80
|
+
t = @processing_thread unless @processing_thread == Thread.current
|
81
|
+
else
|
82
|
+
t = @items[id]
|
83
|
+
unless t.is_a? FG
|
84
|
+
t = @items[id] = FG.new block, self
|
85
|
+
@queue.foreground id
|
86
|
+
run
|
87
|
+
end
|
88
|
+
end
|
89
|
+
}
|
90
|
+
t.join if t
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
def run
|
95
|
+
return unless @state == :idle
|
96
|
+
@state = :running
|
97
|
+
@throttling = Thread.new {
|
98
|
+
loop {
|
99
|
+
break if @queue.shutdown? or @queue.empty?
|
100
|
+
|
101
|
+
elapsed = Time.now - @t0
|
102
|
+
wait_time = 1.0 / @limit + 0.01
|
103
|
+
if @processing_thread and elapsed < wait_time
|
104
|
+
@mutex.synchronize {
|
105
|
+
@pausing.wait @mutex, wait_time - elapsed
|
106
|
+
}
|
107
|
+
end
|
108
|
+
|
109
|
+
if id = @queue.pop
|
110
|
+
@mutex.synchronize {
|
111
|
+
@in_flight = id
|
112
|
+
@processing_thread = Thread.new {
|
113
|
+
block = @items[@in_flight]
|
114
|
+
if block.arity == 0
|
115
|
+
block.call
|
116
|
+
else
|
117
|
+
block.call @in_flight
|
118
|
+
end
|
119
|
+
}
|
120
|
+
}
|
121
|
+
@processing_thread.join if @processing_thread
|
122
|
+
|
123
|
+
@mutex.synchronize {
|
124
|
+
@items.delete @in_flight
|
125
|
+
@in_flight = nil
|
126
|
+
}
|
127
|
+
end
|
128
|
+
|
129
|
+
@t0 = Time.now
|
130
|
+
}
|
131
|
+
|
132
|
+
@mutex.synchronize {
|
133
|
+
@state = :idle
|
134
|
+
if @queue.shutdown? or @queue.empty?
|
135
|
+
@idle.broadcast
|
136
|
+
else
|
137
|
+
# Restart to prevent a join deadlock
|
138
|
+
send :run
|
139
|
+
end
|
140
|
+
}
|
141
|
+
}
|
142
|
+
end
|
143
|
+
class FG #:nodoc: all
|
144
|
+
def initialize(block, h)
|
145
|
+
@block = block
|
146
|
+
@thread = Thread.new {
|
147
|
+
Thread.stop unless @args
|
148
|
+
@block.call *@args
|
149
|
+
}
|
150
|
+
@h = h
|
151
|
+
end
|
152
|
+
def arity
|
153
|
+
@block.arity
|
154
|
+
end
|
155
|
+
def call(*args)
|
156
|
+
@args = args
|
157
|
+
@thread.run
|
158
|
+
end
|
159
|
+
def join
|
160
|
+
@thread.join
|
161
|
+
end
|
162
|
+
end
|
163
|
+
class PriorityQueue #:nodoc: all
|
164
|
+
def initialize
|
165
|
+
@mutex = Mutex.new
|
166
|
+
@fg = []
|
167
|
+
@bg = []
|
168
|
+
@received = ConditionVariable.new
|
169
|
+
@shutdown = false
|
170
|
+
end
|
171
|
+
|
172
|
+
def shutdown
|
173
|
+
@shutdown = true
|
174
|
+
@received.signal
|
175
|
+
end
|
176
|
+
|
177
|
+
def shutdown?
|
178
|
+
@shutdown
|
179
|
+
end
|
180
|
+
|
181
|
+
def empty?
|
182
|
+
@mutex.synchronize {
|
183
|
+
@fg.empty? and @bg.empty?
|
184
|
+
}
|
185
|
+
end
|
186
|
+
|
187
|
+
def background(id)
|
188
|
+
@mutex.synchronize {
|
189
|
+
unless @shutdown || @bg.include?(id)
|
190
|
+
@bg << id
|
191
|
+
@received.signal
|
192
|
+
end
|
193
|
+
}
|
194
|
+
end
|
195
|
+
|
196
|
+
def foreground(id)
|
197
|
+
@mutex.synchronize {
|
198
|
+
unless @shutdown || @fg.include?(id)
|
199
|
+
@fg << id
|
200
|
+
if @bg.include?(id)
|
201
|
+
@bg.delete id
|
202
|
+
else
|
203
|
+
@received.signal
|
204
|
+
end
|
205
|
+
end
|
206
|
+
}
|
207
|
+
end
|
208
|
+
|
209
|
+
def pop
|
210
|
+
@mutex.synchronize {
|
211
|
+
if @fg.empty? and @bg.empty?
|
212
|
+
@received.wait(@mutex) unless @shutdown
|
213
|
+
end
|
214
|
+
|
215
|
+
if @shutdown
|
216
|
+
elsif ! @fg.empty?
|
217
|
+
@fg.shift
|
218
|
+
else
|
219
|
+
@bg.shift
|
220
|
+
end
|
221
|
+
}
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require_relative '../lib/throttle-queue/multi-process'
|
2
|
+
require 'test/unit'
|
3
|
+
require 'thread'
|
4
|
+
|
5
|
+
class ThrottleQueueMultiProcessTest < Test::Unit::TestCase
|
6
|
+
|
7
|
+
def testSingleProcess
|
8
|
+
t = ThrottleQueue::MultiProcess.new 10
|
9
|
+
|
10
|
+
results = []
|
11
|
+
%w(apple banana cake donut egg).each {|w|
|
12
|
+
t.background(w) {
|
13
|
+
results << w.capitalize
|
14
|
+
}
|
15
|
+
}
|
16
|
+
t.wait
|
17
|
+
assert_equal %w(Apple Banana Cake Donut Egg), results
|
18
|
+
ensure
|
19
|
+
t.shutdown
|
20
|
+
end
|
21
|
+
|
22
|
+
def testTwoProcesses
|
23
|
+
p = fork {
|
24
|
+
t = ThrottleQueue::MultiProcess.new 10
|
25
|
+
%w(fig grape ham ice jelly).each {|w|
|
26
|
+
t.background(w) {
|
27
|
+
File.open('results.txt', 'a') {|f|
|
28
|
+
f.puts w.capitalize
|
29
|
+
}
|
30
|
+
}
|
31
|
+
}
|
32
|
+
t.wait
|
33
|
+
}
|
34
|
+
|
35
|
+
t = ThrottleQueue::MultiProcess.new 10
|
36
|
+
|
37
|
+
results = []
|
38
|
+
%w(apple banana cake donut egg).each {|w|
|
39
|
+
t.background(w) {
|
40
|
+
results << w.capitalize
|
41
|
+
}
|
42
|
+
}
|
43
|
+
t.wait
|
44
|
+
assert_equal %w(Apple Banana Cake Donut Egg), results
|
45
|
+
assert_equal %w(Fig Grape Ham Ice Jelly), File.open('results.txt') {|f| f.readlines.map &:chomp}
|
46
|
+
ensure
|
47
|
+
t.shutdown
|
48
|
+
FileUtils.rm_f 'results.txt'
|
49
|
+
end
|
50
|
+
|
51
|
+
def testTwoProcessesWithFG
|
52
|
+
rd, wr = IO.pipe
|
53
|
+
|
54
|
+
p = fork {
|
55
|
+
rd.close
|
56
|
+
t = ThrottleQueue::MultiProcess.new 10
|
57
|
+
%w(fig grape ham ice jelly).each {|w|
|
58
|
+
t.background(w) {
|
59
|
+
File.open('results.txt', 'a') {|f|
|
60
|
+
f.puts w.capitalize
|
61
|
+
if w == 'grape'
|
62
|
+
wr.close
|
63
|
+
end
|
64
|
+
}
|
65
|
+
}
|
66
|
+
}
|
67
|
+
t.wait
|
68
|
+
}
|
69
|
+
|
70
|
+
wr.close
|
71
|
+
|
72
|
+
t = ThrottleQueue::MultiProcess.new 10
|
73
|
+
rd.read
|
74
|
+
rd.close
|
75
|
+
|
76
|
+
t.foreground('apple') {|w|
|
77
|
+
File.open('results.txt', 'a') {|f|
|
78
|
+
f.puts w.capitalize
|
79
|
+
}
|
80
|
+
}
|
81
|
+
|
82
|
+
t.wait
|
83
|
+
assert_equal %w(Fig Grape Apple Ham Ice Jelly), File.open('results.txt') {|f| f.readlines.map &:chomp}
|
84
|
+
ensure
|
85
|
+
t.shutdown
|
86
|
+
FileUtils.rm_f 'results.txt'
|
87
|
+
end
|
88
|
+
end
|
data/test/throttle-queue-test.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require_relative '../lib/throttle-queue'
|
2
2
|
require 'test/unit'
|
3
|
+
require 'thread'
|
3
4
|
|
4
5
|
class ThrottleQueueTest < Test::Unit::TestCase
|
5
6
|
|
@@ -61,7 +62,7 @@ class ThrottleQueueTest < Test::Unit::TestCase
|
|
61
62
|
}
|
62
63
|
|
63
64
|
@t.wait
|
64
|
-
assert_equal %w(Apple Banana
|
65
|
+
assert_equal %w(Apple Banana Cake Donut Egg), results
|
65
66
|
end
|
66
67
|
def testForegroundInFlight
|
67
68
|
results = []
|
@@ -88,7 +89,6 @@ class ThrottleQueueTest < Test::Unit::TestCase
|
|
88
89
|
results << 'CAKEYO'
|
89
90
|
}
|
90
91
|
}
|
91
|
-
|
92
92
|
%w(apple banana cake donut egg).each {|w|
|
93
93
|
@t.background(w) {
|
94
94
|
results << w.capitalize
|
@@ -124,6 +124,32 @@ class ThrottleQueueTest < Test::Unit::TestCase
|
|
124
124
|
@t.wait
|
125
125
|
assert_equal %w(Apple Banana Fish Grape Cake Donut Egg), results
|
126
126
|
end
|
127
|
+
def testForegroundWaitOnQueuedForeground
|
128
|
+
results = []
|
129
|
+
threads = []
|
130
|
+
|
131
|
+
ids = Queue.new
|
132
|
+
ids << 'apple' << 'banana' << 'banana' << 'banana'
|
133
|
+
|
134
|
+
values = Queue.new
|
135
|
+
values << 'Apple' << 'Banana' << 'BANANAYO' << 'DUDE'
|
136
|
+
|
137
|
+
ids.size.times {
|
138
|
+
threads << Thread.new {
|
139
|
+
@t.foreground(ids.pop) {
|
140
|
+
results << values.pop
|
141
|
+
}
|
142
|
+
}
|
143
|
+
}
|
144
|
+
|
145
|
+
threads.each {|t|
|
146
|
+
t.join
|
147
|
+
}
|
148
|
+
@t.wait
|
149
|
+
assert_equal %w(Apple Banana), results
|
150
|
+
assert_equal 0, ids.size
|
151
|
+
assert_equal 2, values.size
|
152
|
+
end
|
127
153
|
def testShutdownWithoutWaiting
|
128
154
|
results = []
|
129
155
|
%w(apple banana cake donut egg).each {|w|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: throttle-queue
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ryan Calhoun
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-
|
11
|
+
date: 2014-12-09 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: A thread-safe rate-limited work queue, which allows for background and
|
14
14
|
foreground operations.
|
@@ -22,6 +22,9 @@ files:
|
|
22
22
|
- README.md
|
23
23
|
- Rakefile
|
24
24
|
- lib/throttle-queue.rb
|
25
|
+
- lib/throttle-queue/multi-process.rb
|
26
|
+
- lib/throttle-queue/single-process.rb
|
27
|
+
- test/multiprocess-test.rb
|
25
28
|
- test/throttle-queue-test.rb
|
26
29
|
homepage: https://github.com/theryan/throttle-queue
|
27
30
|
licenses:
|
@@ -49,4 +52,5 @@ specification_version: 4
|
|
49
52
|
summary: A thread-safe rate-limited work queue
|
50
53
|
test_files:
|
51
54
|
- test/throttle-queue-test.rb
|
55
|
+
- test/multiprocess-test.rb
|
52
56
|
- Rakefile
|