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.
@@ -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