throttle-queue 0.0.1 → 0.1.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 +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
|