throttle-queue 0.0.1
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/LICENSE.txt +21 -0
- data/README.md +48 -0
- data/Rakefile +8 -0
- data/lib/throttle-queue.rb +222 -0
- data/test/throttle-queue-test.rb +141 -0
- metadata +52 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 602c620f6b090b5696a141185d5e1d65c8240c07
|
4
|
+
data.tar.gz: 852eec61c6d3d3c1200e83b63a202d03d57a4842
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 53b009d91bafd808374a49e10a98e19fd926f98bee250e163f77bf9da7ffad395528ec33df9855e57b75f338dd3579c7ce5234b1a6954df8769470700511a6e3
|
7
|
+
data.tar.gz: a430b7bb92bde90fe9bf937198b2337171bf98292d4ae72fbc3924998b827d9298c33020c3fa939bffaad021107533992110aebc8c11fbe8ca15e39e42f7cfe3
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2014 Ryan Calhoun
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
[](http://badge.fury.io/rb/throttle-queue)
|
2
|
+
|
3
|
+
# ThrottleQueue
|
4
|
+
|
5
|
+
A thread-safe rate-limited work queue, with foreground and background operations
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
gem 'throttle-queue'
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install throttle-queue
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
Create a queue and add background work
|
24
|
+
|
25
|
+
q = ThrottleQueue.new 3
|
26
|
+
files.each {|file|
|
27
|
+
q.background(file) {
|
28
|
+
fetch file
|
29
|
+
}
|
30
|
+
}
|
31
|
+
|
32
|
+
Get user input and take action right away
|
33
|
+
|
34
|
+
q.foreground(user_file) {
|
35
|
+
fetch user_file
|
36
|
+
}
|
37
|
+
|
38
|
+
Wait for everything to finish
|
39
|
+
|
40
|
+
q.wait
|
41
|
+
|
42
|
+
## Contributing
|
43
|
+
|
44
|
+
1. Fork it
|
45
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
46
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
47
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
48
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,222 @@
|
|
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
|
+
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
|
@@ -0,0 +1,141 @@
|
|
1
|
+
require_relative '../lib/throttle-queue'
|
2
|
+
require 'test/unit'
|
3
|
+
|
4
|
+
class ThrottleQueueTest < Test::Unit::TestCase
|
5
|
+
|
6
|
+
def setup
|
7
|
+
@t = ThrottleQueue.new 10
|
8
|
+
end
|
9
|
+
|
10
|
+
def teardown
|
11
|
+
@t.shutdown
|
12
|
+
end
|
13
|
+
|
14
|
+
def testBackground
|
15
|
+
results = []
|
16
|
+
%w(apple banana cake donut egg).each {|w|
|
17
|
+
@t.background(w) {
|
18
|
+
results << w.capitalize
|
19
|
+
}
|
20
|
+
}
|
21
|
+
@t.wait
|
22
|
+
assert_equal %w(Apple Banana Cake Donut Egg), results
|
23
|
+
end
|
24
|
+
def testForeground
|
25
|
+
results = []
|
26
|
+
%w(apple banana cake donut egg).each_with_index {|w,i|
|
27
|
+
@t.foreground(w) {
|
28
|
+
results << w.capitalize
|
29
|
+
}
|
30
|
+
assert_equal %w(Apple Banana Cake Donut Egg)[0..i], results
|
31
|
+
}
|
32
|
+
@t.wait
|
33
|
+
end
|
34
|
+
def testBackgroundInFlight
|
35
|
+
results = []
|
36
|
+
%w(apple banana cake donut egg).each {|w|
|
37
|
+
@t.background(w) {
|
38
|
+
results << w.capitalize
|
39
|
+
if w == 'banana'
|
40
|
+
@t.background('banana') {
|
41
|
+
results << 'BANANAYO'
|
42
|
+
}
|
43
|
+
end
|
44
|
+
}
|
45
|
+
}
|
46
|
+
|
47
|
+
@t.wait
|
48
|
+
assert_equal %w(Apple Banana Cake Donut Egg), results
|
49
|
+
end
|
50
|
+
def testBackgroundQueued
|
51
|
+
results = []
|
52
|
+
%w(apple banana cake donut egg).each {|w|
|
53
|
+
@t.background(w) {
|
54
|
+
results << w.capitalize
|
55
|
+
if w == 'banana'
|
56
|
+
@t.background('cake') {
|
57
|
+
results << 'CAKEYO'
|
58
|
+
}
|
59
|
+
end
|
60
|
+
}
|
61
|
+
}
|
62
|
+
|
63
|
+
@t.wait
|
64
|
+
assert_equal %w(Apple Banana CAKEYO Donut Egg), results
|
65
|
+
end
|
66
|
+
def testForegroundInFlight
|
67
|
+
results = []
|
68
|
+
%w(apple banana cake donut egg).each {|w|
|
69
|
+
@t.background(w) {
|
70
|
+
results << w.capitalize
|
71
|
+
if w == 'banana'
|
72
|
+
@t.foreground('banana') {
|
73
|
+
results << 'BANANAYO'
|
74
|
+
}
|
75
|
+
end
|
76
|
+
}
|
77
|
+
}
|
78
|
+
|
79
|
+
@t.wait
|
80
|
+
assert_equal %w(Apple Banana Cake Donut Egg), results
|
81
|
+
end
|
82
|
+
def testForegroundQueued
|
83
|
+
results = []
|
84
|
+
|
85
|
+
t = Thread.new {
|
86
|
+
Thread.stop
|
87
|
+
@t.foreground('cake') {
|
88
|
+
results << 'CAKEYO'
|
89
|
+
}
|
90
|
+
}
|
91
|
+
|
92
|
+
%w(apple banana cake donut egg).each {|w|
|
93
|
+
@t.background(w) {
|
94
|
+
results << w.capitalize
|
95
|
+
if w == 'banana'
|
96
|
+
t.run
|
97
|
+
end
|
98
|
+
}
|
99
|
+
}
|
100
|
+
|
101
|
+
@t.wait
|
102
|
+
assert_equal %w(Apple Banana CAKEYO Donut Egg), results
|
103
|
+
end
|
104
|
+
def testForegroundPreemptBackground
|
105
|
+
results = []
|
106
|
+
|
107
|
+
t = Thread.new {
|
108
|
+
Thread.stop
|
109
|
+
%w(fish grape).each {|w|
|
110
|
+
@t.foreground(w) {
|
111
|
+
results << w.capitalize
|
112
|
+
}
|
113
|
+
}
|
114
|
+
}
|
115
|
+
%w(apple banana cake donut egg).each {|w|
|
116
|
+
@t.background(w) {
|
117
|
+
results << w.capitalize
|
118
|
+
if w == 'banana'
|
119
|
+
t.run
|
120
|
+
end
|
121
|
+
}
|
122
|
+
}
|
123
|
+
|
124
|
+
@t.wait
|
125
|
+
assert_equal %w(Apple Banana Fish Grape Cake Donut Egg), results
|
126
|
+
end
|
127
|
+
def testShutdownWithoutWaiting
|
128
|
+
results = []
|
129
|
+
%w(apple banana cake donut egg).each {|w|
|
130
|
+
@t.background(w) {
|
131
|
+
results << w.capitalize
|
132
|
+
if w == 'banana'
|
133
|
+
@t.shutdown
|
134
|
+
end
|
135
|
+
}
|
136
|
+
}
|
137
|
+
@t.wait
|
138
|
+
assert_equal %w(Apple Banana), results
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
metadata
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: throttle-queue
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ryan Calhoun
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-11-20 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: A thread-safe rate-limited work queue, which allows for background and
|
14
|
+
foreground operations.
|
15
|
+
email:
|
16
|
+
- ryanjamescalhoun@gmail.com
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- LICENSE.txt
|
22
|
+
- README.md
|
23
|
+
- Rakefile
|
24
|
+
- lib/throttle-queue.rb
|
25
|
+
- test/throttle-queue-test.rb
|
26
|
+
homepage: https://github.com/theryan/throttle-queue
|
27
|
+
licenses:
|
28
|
+
- MIT
|
29
|
+
metadata: {}
|
30
|
+
post_install_message:
|
31
|
+
rdoc_options: []
|
32
|
+
require_paths:
|
33
|
+
- lib
|
34
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
35
|
+
requirements:
|
36
|
+
- - ">="
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: '0'
|
39
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
requirements: []
|
45
|
+
rubyforge_project:
|
46
|
+
rubygems_version: 2.2.2
|
47
|
+
signing_key:
|
48
|
+
specification_version: 4
|
49
|
+
summary: A thread-safe rate-limited work queue
|
50
|
+
test_files:
|
51
|
+
- test/throttle-queue-test.rb
|
52
|
+
- Rakefile
|