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