throttle-queue 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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.
@@ -0,0 +1,48 @@
1
+ [![Gem Version](https://badge.fury.io/rb/throttle-queue.svg)](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
@@ -0,0 +1,8 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new {|t|
4
+ t.test_files = FileList['test/**/*est.rb']
5
+ }
6
+
7
+ desc 'Run tests'
8
+ task :default => :test
@@ -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