work_queue 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2009 Miguel Fonseca
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,11 @@
1
+ Conceptually, a work queue is designed to coordinate work between a producer and a set of worker threads.
2
+ When some task needs to be performed, the producer adds an object containing the task routine to the work queue.
3
+ If the work queue is full, the producer will block until a worker thread removes an object from the queue.
4
+ Eventually, one of the worker threads removes the object from the work queue and executes the routine.
5
+ If the work queue is empty, a worker thread will block until an object is made available by the producer or until the keep-alive time expires.
6
+
7
+ Work queues are useful for several reasons:
8
+ * To easily perform tasks asynchronously and concurrently in your application;
9
+ * To let you focus on the work you actually want to perform without having to worry about the thread creation and management;
10
+ * To minimize overhead, by reusing previously constructed threads rather than creating new ones;
11
+ * To bound resource use, by setting a limit on the maximum number of simultaneously executing threads;
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/clean'
4
+
5
+ # Load all rakefile extensions
6
+ Dir["#{File.dirname(__FILE__)}/tasks/*.rake"].each { |ext| load ext }
7
+
8
+ # Set default task
9
+ task :default => ["test:unit"]
data/lib/work_queue.rb ADDED
@@ -0,0 +1,237 @@
1
+ ##
2
+ # = Name
3
+ # WorkQueue
4
+ #
5
+ # == Description
6
+ # This file contains an implementation of a work queue structure.
7
+ #
8
+ # == Version
9
+ # 0.1.0
10
+ #
11
+ # == Author
12
+ # Miguel Fonseca <fmmfonseca@gmail.com>
13
+ #
14
+ # == Copyright
15
+ # Copyright 2009 Miguel Fonseca
16
+ #
17
+ # == License
18
+ # MIT (see LICENSE file)
19
+ #
20
+
21
+ require 'thread'
22
+ require 'timeout'
23
+
24
+ ##
25
+ # = WorkQueue
26
+ #
27
+ # == Description
28
+ # A tunable work queue, designed to coordinate work between a producer and a set of worker threads.
29
+ #
30
+ # == Usage
31
+ # wq = WorkQueue.new(1)
32
+ # wq.enqueue_b { puts "Hello from the WorkQueue" }
33
+ # wq.join
34
+ #
35
+ class WorkQueue
36
+
37
+ VERSION = "0.1.0"
38
+
39
+ ##
40
+ # Creates a new work queue.
41
+ #
42
+ # wq = WorkQueue.new(5,10,20)
43
+ #
44
+ def initialize(max_threads=nil, max_tasks=nil, keep_alive=60)
45
+ self.max_threads = max_threads
46
+ self.max_tasks = max_tasks
47
+ self.keep_alive = keep_alive
48
+ @threads = []
49
+ @threads_lock = Mutex.new
50
+ @tasks = max_tasks ? SizedQueue.new(max_tasks) : Queue.new
51
+ @threads.taint
52
+ @tasks.taint
53
+ self.taint
54
+ end
55
+
56
+ ##
57
+ # Returns the maximum number of worker threads.
58
+ # This value is set upon initialization and cannot be changed afterwards.
59
+ #
60
+ # wq = WorkQueue.new(1)
61
+ # wq.max_threads #=> 1
62
+ #
63
+ def max_threads
64
+ @max_threads
65
+ end
66
+
67
+ ##
68
+ # Returns the current number of worker threads.
69
+ # This value is just a snapshot, and may change immediately upon returning.
70
+ #
71
+ # wq = WorkQueue.new(10)
72
+ # wq.cur_threads #=> 0
73
+ # wq.enqueue_b {}
74
+ # wq.cur_threads #=> 1
75
+ #
76
+ def cur_threads
77
+ @threads.size
78
+ end
79
+
80
+ ##
81
+ # Returns the maximum number of queued tasks.
82
+ # This value is set upon initialization and cannot be changed afterwards.
83
+ #
84
+ # wq = WorkQueue.new(nil,1)
85
+ # wq.max_tasks #=> 1
86
+ #
87
+ def max_tasks
88
+ @max_tasks
89
+ end
90
+
91
+ ##
92
+ # Returns the current number of queued tasks.
93
+ # This value is just a snapshot, and may change immediately upon returning.
94
+ #
95
+ # wq = WorkQueue.new(1)
96
+ # wq.enqueue_b { sleep(1) }
97
+ # wq.cur_tasks #=> 0
98
+ # wq.enqueue_b {}
99
+ # wq.cur_tasks #=> 1
100
+ #
101
+ def cur_tasks
102
+ @tasks.size
103
+ end
104
+
105
+ ##
106
+ # Returns the number of seconds to keep worker threads alive waiting for new tasks.
107
+ # This value is set upon initialization and cannot be changed afterwards.
108
+ #
109
+ # wq = WorkQueue.new()
110
+ # wq.keep_alive #=> 60
111
+ # wq = WorkQueue.new(nil,nil,1)
112
+ # wq.keep_alive #=> 1
113
+ #
114
+ def keep_alive
115
+ @keep_alive
116
+ end
117
+
118
+ ##
119
+ # Schedules the given Proc for execution by a worker thread.
120
+ # If there is no space left in the queue, waits until space becomes available.
121
+ #
122
+ # wq = WorkQueue.new(1)
123
+ # wq.enqueue_p(Proc.new {})
124
+ #
125
+ def enqueue_p(proc, *args)
126
+ @tasks << [proc,args]
127
+ spawn_thread
128
+ self
129
+ end
130
+
131
+ ##
132
+ # Schedules the given Block for execution by a worker thread.
133
+ # If there is no space left in the queue, waits until space becomes available.
134
+ #
135
+ # wq = WorkQueue.new(1)
136
+ # wq.enqueue_b {}
137
+ #
138
+ def enqueue_b(*args, &block)
139
+ @tasks << [block,args]
140
+ spawn_thread
141
+ self
142
+ end
143
+
144
+ ##
145
+ # Waits until the tasks queue is empty.
146
+ #
147
+ # wq = WorkQueue.new(1)
148
+ # wq.enqueue_b { sleep(1) }
149
+ # wq.join
150
+ #
151
+ def join
152
+ cur_threads.times { dismiss_thread }
153
+ @threads.dup.each { |t| t.join }
154
+ self
155
+ end
156
+
157
+ ##
158
+ # Stops all worker threads immediately, aborting any ongoing tasks.
159
+ #
160
+ # wq = WorkQueue.new(1)
161
+ # wq.enqueue_b { sleep(1) }
162
+ # wq.stop
163
+ #
164
+ def stop
165
+ @threads.dup.each { |t| t.exit.join }
166
+ @tasks.clear
167
+ self
168
+ end
169
+
170
+ private
171
+
172
+ ##
173
+ # Sets the maximum number of worker threads.
174
+ #
175
+ def max_threads=(value)
176
+ raise ArgumentError, "the maximum number of threads must be positive" if value and value <= 0
177
+ @max_threads = value || 1.0/0
178
+ end
179
+
180
+ ##
181
+ # Sets the maximum number of queued tasks.
182
+ #
183
+ def max_tasks=(value)
184
+ raise ArgumentError, "the maximum number of tasks must be positive" if value and value <= 0
185
+ @max_tasks = value || 1.0/0
186
+ end
187
+
188
+ ##
189
+ # Sets the maximum time to keep worker threads alive waiting for new tasks.
190
+ #
191
+ def keep_alive=(value)
192
+ raise ArgumentError, "the keep-alive time must be positive" if value and value <= 0
193
+ @keep_alive = value || 1.0/0
194
+ end
195
+
196
+ ##
197
+ # Spawns a new worker thread.
198
+ #
199
+ def spawn_thread
200
+ if cur_threads < max_threads and @tasks.num_waiting <= 0
201
+ @threads_lock.synchronize {
202
+ @threads << Thread.new do
203
+ begin
204
+ work()
205
+ ensure
206
+ @threads_lock.synchronize { @threads.delete(Thread.current) }
207
+ end
208
+ end
209
+ }
210
+ end
211
+ end
212
+
213
+ ##
214
+ # Dismisses an existing worker thread.
215
+ #
216
+ def dismiss_thread
217
+ @tasks << [Proc.new { Thread.exit }, nil] if cur_threads > 0
218
+ end
219
+
220
+ ##
221
+ # Repeatedly process the tasks queue.
222
+ #
223
+ def work
224
+ loop do
225
+ begin
226
+ proc, args = timeout(keep_alive) { @tasks.pop }
227
+ proc.call(*args)
228
+ rescue Timeout::Error
229
+ break
230
+ rescue Exception
231
+ # suppress exception
232
+ end
233
+ break if cur_threads > max_threads
234
+ end
235
+ end
236
+
237
+ end
data/tasks/gem.rake ADDED
@@ -0,0 +1,26 @@
1
+ require 'rake/gempackagetask'
2
+ require 'lib/work_queue'
3
+
4
+ CLEAN.include("pkg")
5
+
6
+ # For a list of all attributes refer to http://docs.rubygems.org/read/chapter/20
7
+ spec = Gem::Specification.new do |s|
8
+ s.name = "work_queue"
9
+ s.version = WorkQueue::VERSION
10
+ s.summary = "A tunable work queue, designed to coordinate work between a producer and a set of worker threads."
11
+ s.homepage = "http://github.com/fmmfonseca/work_queue"
12
+ s.author = "Miguel Fonseca"
13
+ s.email = "fmmfonseca@gmail.com"
14
+
15
+ s.required_ruby_version = ">= 1.8.6"
16
+ s.files = FileList["LICENSE", "Rakefile", "README.rdoc", "tasks/*.rake", "lib/**/*.rb", "test/tc_*.rb"].to_a
17
+ s.test_files = Dir.glob("test/tc_*.rb")
18
+ s.has_rdoc = true
19
+ s.extra_rdoc_files = ["README.rdoc"]
20
+ s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "WorkQueue", "--main", "README.rdoc"]
21
+ end
22
+
23
+ # For a list of all attributes refer to http://rake.rubyforge.org/classes/Rake/PackageTask.html
24
+ Rake::GemPackageTask.new(spec) do |p|
25
+ p.need_zip = true
26
+ end
data/tasks/rdoc.rake ADDED
@@ -0,0 +1,10 @@
1
+ require 'rake/rdoctask'
2
+
3
+ CLEAN.include("doc")
4
+
5
+ # For a list of all attributes refer to http://rake.rubyforge.org/classes/Rake/RDocTask.html
6
+ Rake::RDocTask.new do |rd|
7
+ rd.main = "README.rdoc"
8
+ rd.rdoc_files.include("README.rdoc", "lib/**/*.rb")
9
+ rd.rdoc_dir = "doc"
10
+ end
data/tasks/test.rake ADDED
@@ -0,0 +1,22 @@
1
+ require 'rake/testtask'
2
+
3
+ namespace(:test) do
4
+
5
+ # For a list of all attributes refer to http://rake.rubyforge.org/classes/Rake/TestTask.html
6
+ Rake::TestTask.new(:unit) do |t|
7
+ t.libs << "test"
8
+ t.test_files = FileList['test/tc_*.rb']
9
+ t.verbose = true
10
+ t.warning = true
11
+ end
12
+
13
+ desc "Run tests on multiple ruby versions"
14
+ task(:compatibility) do
15
+ versions = %w[1.8.6 1.8.7 1.9.1 jruby]
16
+ system <<-CMD
17
+ bash -c 'source ~/.rvm/scripts/rvm;
18
+ rvm #{versions.join(',')} rake test:unit'
19
+ CMD
20
+ end
21
+
22
+ end
@@ -0,0 +1,89 @@
1
+ ##
2
+ # = Name
3
+ # TC_WorkQueue
4
+ #
5
+ # == Description
6
+ # This file contains unit tests for the WorkQueue class.
7
+ #
8
+ # == Author
9
+ # Miguel Fonseca <fmmfonseca@gmail.com>
10
+ #
11
+ # == Copyright
12
+ # Copyright 2009 Miguel Fonseca
13
+ #
14
+ # == License
15
+ # MIT (see LICENSE file)
16
+
17
+ require 'test/unit'
18
+ require 'lib/work_queue'
19
+
20
+ class TC_WorkQueue < Test::Unit::TestCase
21
+
22
+ # def setup
23
+ # end
24
+
25
+ # def teardown
26
+ # end
27
+
28
+ def test_enqueue
29
+ s = String.new
30
+ wq = WorkQueue.new
31
+ # Using Proc
32
+ wq.enqueue_p(Proc.new { |str| str.replace("Hello #1") }, s)
33
+ wq.join
34
+ assert_equal(s, "Hello #1")
35
+ # Using Block
36
+ wq.enqueue_b(s) { |str| str.replace("Hello #2") }
37
+ wq.join
38
+ assert_equal(s, "Hello #2")
39
+ end
40
+
41
+ def test_max_threads
42
+ assert_raise(ArgumentError) { WorkQueue.new(0) }
43
+ assert_raise(ArgumentError) { WorkQueue.new(-1) }
44
+ wq = WorkQueue.new(1)
45
+ assert_equal(wq.cur_threads, 0)
46
+ wq.enqueue_b { sleep(0.01) }
47
+ assert_equal(wq.cur_threads, 1)
48
+ wq.enqueue_b { sleep(0.01) }
49
+ assert_equal(wq.cur_threads, 1)
50
+ sleep(0.1)
51
+ assert_equal(wq.cur_threads, 1)
52
+ wq.join
53
+ end
54
+
55
+ def test_max_tasks
56
+ assert_raise(ArgumentError) { WorkQueue.new(nil,0) }
57
+ assert_raise(ArgumentError) { WorkQueue.new(nil,-1) }
58
+ wq = WorkQueue.new(1,1)
59
+ wq.enqueue_b { sleep(0.01) }
60
+ wq.enqueue_b { sleep(0.01) }
61
+ assert_equal(wq.cur_tasks, 1)
62
+ wq.enqueue_b { sleep(0.01) }
63
+ assert_equal(wq.cur_tasks, 1)
64
+ wq.join
65
+ end
66
+
67
+ def test_keep_alive
68
+ assert_raise(ArgumentError) { WorkQueue.new(nil,nil,0) }
69
+ assert_raise(ArgumentError) { WorkQueue.new(nil,nil,-1) }
70
+ wq = WorkQueue.new(1,1,0.01)
71
+ wq.enqueue_b { sleep(0.01) }
72
+ assert_equal(wq.cur_threads, 1)
73
+ sleep(0.1)
74
+ assert_equal(wq.cur_threads, 0)
75
+ wq.join
76
+ end
77
+
78
+ def test_stress
79
+ a = []
80
+ m = Mutex.new
81
+ wq = WorkQueue.new(100)
82
+ (1..1000).each {
83
+ wq.enqueue_b(a,m) { |str,mut| sleep(0.01); mut.synchronize { a.push nil } }
84
+ }
85
+ wq.join
86
+ assert_equal(a.size, 1000)
87
+ end
88
+
89
+ end
metadata ADDED
@@ -0,0 +1,67 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: work_queue
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Miguel Fonseca
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-11-22 00:00:00 +00:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description:
17
+ email: fmmfonseca@gmail.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README.rdoc
24
+ files:
25
+ - LICENSE
26
+ - Rakefile
27
+ - README.rdoc
28
+ - tasks/gem.rake
29
+ - tasks/rdoc.rake
30
+ - tasks/test.rake
31
+ - lib/work_queue.rb
32
+ - test/tc_work_queue.rb
33
+ has_rdoc: true
34
+ homepage: http://github.com/fmmfonseca/work_queue
35
+ licenses: []
36
+
37
+ post_install_message:
38
+ rdoc_options:
39
+ - --line-numbers
40
+ - --inline-source
41
+ - --title
42
+ - WorkQueue
43
+ - --main
44
+ - README.rdoc
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: 1.8.6
52
+ version:
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: "0"
58
+ version:
59
+ requirements: []
60
+
61
+ rubyforge_project:
62
+ rubygems_version: 1.3.5
63
+ signing_key:
64
+ specification_version: 3
65
+ summary: A tunable work queue, designed to coordinate work between a producer and a set of worker threads.
66
+ test_files:
67
+ - test/tc_work_queue.rb