work_queue 1.0.0 → 2.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.
- data/README.rdoc +1 -3
- data/lib/work_queue.rb +200 -207
- data/tasks/test.rake +0 -9
- data/test/tc_work_queue.rb +31 -12
- metadata +19 -7
data/README.rdoc
CHANGED
@@ -4,7 +4,7 @@ A work queue is designed to coordinate work between a producer and a pool of wor
|
|
4
4
|
When some task needs to be performed, the producer adds an object containing the task routine to the work queue.
|
5
5
|
If the work queue is full, the producer will block until a worker thread removes an object from the queue.
|
6
6
|
Eventually, one of the worker threads removes the object from the work queue and executes the routine.
|
7
|
-
If the work queue is empty, a worker thread will block until an object is made available by the producer
|
7
|
+
If the work queue is empty, a worker thread will block until an object is made available by the producer.
|
8
8
|
|
9
9
|
Work queues are useful for several reasons:
|
10
10
|
* To easily perform tasks asynchronously and concurrently in your application;
|
@@ -32,5 +32,3 @@ Note that you generally want to bound the resources used:
|
|
32
32
|
WorkQueue.new(10)
|
33
33
|
# Limit the maximum number of queued tasks
|
34
34
|
WorkQueue.new(nil,20)
|
35
|
-
# Limit the maximum time to preserve idle worker threads
|
36
|
-
WorkQueue.new(nil,nil,10)
|
data/lib/work_queue.rb
CHANGED
@@ -6,20 +6,20 @@
|
|
6
6
|
# This file contains an implementation of a work queue structure.
|
7
7
|
#
|
8
8
|
# == Version
|
9
|
-
#
|
9
|
+
# 2.0.0
|
10
10
|
#
|
11
11
|
# == Author
|
12
12
|
# Miguel Fonseca <fmmfonseca@gmail.com>
|
13
13
|
#
|
14
14
|
# == Copyright
|
15
|
-
# Copyright 2009-
|
15
|
+
# Copyright 2009-2011 Miguel Fonseca
|
16
16
|
#
|
17
17
|
# == License
|
18
18
|
# MIT (see LICENSE file)
|
19
19
|
#
|
20
20
|
|
21
21
|
require 'thread'
|
22
|
-
require '
|
22
|
+
require 'monitor'
|
23
23
|
|
24
24
|
##
|
25
25
|
# = WorkQueue
|
@@ -33,211 +33,204 @@ require 'timeout'
|
|
33
33
|
# wq.join
|
34
34
|
#
|
35
35
|
class WorkQueue
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
def enqueue_b(*args, &block)
|
143
|
-
@tasks << [block,args]
|
144
|
-
spawn_thread
|
145
|
-
self
|
146
|
-
end
|
147
|
-
|
148
|
-
##
|
149
|
-
# Waits until the tasks queue is empty and all worker threads have finished.
|
150
|
-
#
|
151
|
-
# wq = WorkQueue.new(1)
|
152
|
-
# wq.enqueue_b { sleep(1) }
|
153
|
-
# wq.join
|
154
|
-
#
|
155
|
-
def join
|
156
|
-
cur_threads.times { dismiss_thread }
|
157
|
-
@threads.dup.each { |thread| thread.join }
|
158
|
-
self
|
159
|
-
end
|
160
|
-
|
161
|
-
##
|
162
|
-
# Stops all worker threads immediately, aborting any ongoing tasks.
|
163
|
-
#
|
164
|
-
# wq = WorkQueue.new(1)
|
165
|
-
# wq.enqueue_b { sleep(1) }
|
166
|
-
# wq.stop
|
167
|
-
#
|
168
|
-
def stop
|
169
|
-
@threads.dup.each { |thread| thread.exit.join }
|
170
|
-
@tasks.clear
|
171
|
-
self
|
172
|
-
end
|
173
|
-
|
174
|
-
private
|
175
|
-
|
176
|
-
##
|
177
|
-
# Sets the maximum number of worker threads.
|
178
|
-
#
|
179
|
-
def max_threads=(value)
|
180
|
-
raise ArgumentError, "the maximum number of threads must be positive" if value and value <= 0
|
181
|
-
@max_threads = value || 1.0/0
|
182
|
-
end
|
183
|
-
|
184
|
-
##
|
185
|
-
# Sets the maximum number of queued tasks.
|
186
|
-
#
|
187
|
-
def max_tasks=(value)
|
188
|
-
raise ArgumentError, "the maximum number of tasks must be positive" if value and value <= 0
|
189
|
-
@max_tasks = value || 1.0/0
|
190
|
-
end
|
191
|
-
|
192
|
-
##
|
193
|
-
# Sets the maximum time to keep worker threads alive waiting for new tasks.
|
194
|
-
#
|
195
|
-
def keep_alive=(value)
|
196
|
-
raise ArgumentError, "the keep-alive time must be positive" if value and value <= 0
|
197
|
-
@keep_alive = value || 1.0/0
|
198
|
-
end
|
199
|
-
|
200
|
-
##
|
201
|
-
# Enrolls a new worker thread.
|
202
|
-
# The request is only carried out if necessary.
|
203
|
-
#
|
204
|
-
def spawn_thread
|
205
|
-
if cur_threads < max_threads and @tasks.num_waiting <= 0 and cur_tasks > 0
|
206
|
-
@threads_lock.synchronize {
|
207
|
-
@threads << Thread.new do
|
208
|
-
begin
|
209
|
-
work()
|
210
|
-
ensure
|
211
|
-
@threads_lock.synchronize { @threads.delete(Thread.current) }
|
212
|
-
end
|
36
|
+
|
37
|
+
VERSION = "2.0.1"
|
38
|
+
|
39
|
+
##
|
40
|
+
# Creates a new work queue with the desired parameters.
|
41
|
+
#
|
42
|
+
# wq = WorkQueue.new(5,10,20)
|
43
|
+
#
|
44
|
+
def initialize(max_threads=nil, max_tasks=nil)
|
45
|
+
self.max_threads = max_threads
|
46
|
+
self.max_tasks = max_tasks
|
47
|
+
@threads = Array.new
|
48
|
+
@threads.extend(MonitorMixin)
|
49
|
+
@threads_waiting = 0
|
50
|
+
@tasks = Array.new
|
51
|
+
@tasks.extend(MonitorMixin)
|
52
|
+
@task_enqueued = @tasks.new_cond
|
53
|
+
@task_completed = @tasks.new_cond
|
54
|
+
@cur_tasks = 0
|
55
|
+
end
|
56
|
+
|
57
|
+
##
|
58
|
+
# Returns the maximum number of worker threads.
|
59
|
+
# This value is set upon initialization and cannot be changed afterwards.
|
60
|
+
#
|
61
|
+
# wq = WorkQueue.new()
|
62
|
+
# wq.max_threads #=> Infinity
|
63
|
+
# wq = WorkQueue.new(1)
|
64
|
+
# wq.max_threads #=> 1
|
65
|
+
#
|
66
|
+
def max_threads
|
67
|
+
@max_threads
|
68
|
+
end
|
69
|
+
|
70
|
+
##
|
71
|
+
# Returns the current number of worker threads.
|
72
|
+
# This value is just a snapshot, and may change immediately upon returning.
|
73
|
+
#
|
74
|
+
# wq = WorkQueue.new(10)
|
75
|
+
# wq.cur_threads #=> 0
|
76
|
+
# wq.enqueue_b {}
|
77
|
+
# wq.cur_threads #=> 1
|
78
|
+
#
|
79
|
+
def cur_threads
|
80
|
+
@threads.size
|
81
|
+
end
|
82
|
+
|
83
|
+
##
|
84
|
+
# Returns the maximum number of queued tasks.
|
85
|
+
# This value is set upon initialization and cannot be changed afterwards.
|
86
|
+
#
|
87
|
+
# wq = WorkQueue.new()
|
88
|
+
# wq.max_tasks #=> Infinity
|
89
|
+
# wq = WorkQueue.new(nil,1)
|
90
|
+
# wq.max_tasks #=> 1
|
91
|
+
#
|
92
|
+
def max_tasks
|
93
|
+
@max_tasks
|
94
|
+
end
|
95
|
+
|
96
|
+
##
|
97
|
+
# Returns the current number of active tasks.
|
98
|
+
# This value is just a snapshot, and may change immediately upon returning.
|
99
|
+
#
|
100
|
+
# wq = WorkQueue.new(1)
|
101
|
+
# wq.enqueue_b { sleep(1) }
|
102
|
+
# wq.cur_tasks #=> 0
|
103
|
+
# wq.enqueue_b {}
|
104
|
+
# wq.cur_tasks #=> 1
|
105
|
+
#
|
106
|
+
def cur_tasks
|
107
|
+
@cur_tasks
|
108
|
+
end
|
109
|
+
|
110
|
+
##
|
111
|
+
# Schedules the given Proc for future execution by a worker thread.
|
112
|
+
# If there is no space left in the queue, waits until space becomes available.
|
113
|
+
#
|
114
|
+
# wq = WorkQueue.new(1)
|
115
|
+
# wq.enqueue_p(Proc.new {})
|
116
|
+
#
|
117
|
+
def enqueue_p(proc, *args)
|
118
|
+
enqueue(proc, args)
|
119
|
+
end
|
120
|
+
|
121
|
+
##
|
122
|
+
# Schedules the given Block for future execution by a worker thread.
|
123
|
+
# If there is no space left in the queue, waits until space becomes available.
|
124
|
+
#
|
125
|
+
# wq = WorkQueue.new(1)
|
126
|
+
# wq.enqueue_b {}
|
127
|
+
#
|
128
|
+
def enqueue_b(*args, &block)
|
129
|
+
enqueue(block, args)
|
130
|
+
end
|
131
|
+
|
132
|
+
##
|
133
|
+
# Waits until the tasks queue is empty and all worker threads have finished.
|
134
|
+
#
|
135
|
+
# wq = WorkQueue.new(1)
|
136
|
+
# wq.enqueue_b { sleep(1) }
|
137
|
+
# wq.join
|
138
|
+
#
|
139
|
+
def join
|
140
|
+
@tasks.synchronize do
|
141
|
+
@task_completed.wait_while { cur_tasks > 0 }
|
213
142
|
end
|
214
|
-
}
|
215
143
|
end
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
loop do
|
231
|
-
begin
|
232
|
-
proc, args = timeout(keep_alive) { @tasks.pop }
|
233
|
-
proc.call(*args)
|
234
|
-
rescue Timeout::Error
|
235
|
-
break
|
236
|
-
rescue Exception
|
237
|
-
# suppress exception
|
238
|
-
end
|
239
|
-
break if cur_threads > max_threads
|
144
|
+
|
145
|
+
##
|
146
|
+
# Stops all worker threads immediately, aborting any ongoing tasks.
|
147
|
+
#
|
148
|
+
# wq = WorkQueue.new(1)
|
149
|
+
# wq.enqueue_b { sleep(1) }
|
150
|
+
# wq.kill
|
151
|
+
#
|
152
|
+
def kill
|
153
|
+
@threads.dup.each { |thread| thread.exit.join }
|
154
|
+
@threads.clear
|
155
|
+
@threads_waiting = 0
|
156
|
+
@tasks.clear
|
157
|
+
@cur_tasks = 0
|
240
158
|
end
|
241
|
-
|
242
|
-
|
159
|
+
|
160
|
+
private
|
161
|
+
|
162
|
+
##
|
163
|
+
# Generic
|
164
|
+
#
|
165
|
+
def enqueue(proc, args)
|
166
|
+
@tasks.synchronize do
|
167
|
+
@task_completed.wait_while { cur_tasks >= max_tasks }
|
168
|
+
@tasks << [proc, args]
|
169
|
+
@cur_tasks += 1
|
170
|
+
@task_enqueued.signal
|
171
|
+
spawn_thread
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
##
|
176
|
+
# Sets the maximum number of worker threads.
|
177
|
+
#
|
178
|
+
def max_threads=(value)
|
179
|
+
raise ArgumentError, "the maximum number of threads must be positive" if value and value <= 0
|
180
|
+
@max_threads = value || 1.0/0
|
181
|
+
end
|
182
|
+
|
183
|
+
##
|
184
|
+
# Sets the maximum number of queued tasks.
|
185
|
+
#
|
186
|
+
def max_tasks=(value)
|
187
|
+
raise ArgumentError, "the maximum number of tasks must be positive" if value and value <= 0
|
188
|
+
@max_tasks = value || 1.0/0
|
189
|
+
end
|
190
|
+
|
191
|
+
##
|
192
|
+
# Enrolls a new worker thread.
|
193
|
+
# The request is only carried out if necessary.
|
194
|
+
#
|
195
|
+
def spawn_thread
|
196
|
+
@threads.synchronize do
|
197
|
+
if cur_threads < max_threads and @threads_waiting <= 0 and @tasks.size > 0
|
198
|
+
@threads << Thread.new do
|
199
|
+
begin
|
200
|
+
work
|
201
|
+
ensure
|
202
|
+
@threads.synchronize do
|
203
|
+
@threads.delete(Thread.current)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
|
212
|
+
##
|
213
|
+
# Repeatedly process the tasks queue.
|
214
|
+
#
|
215
|
+
def work
|
216
|
+
loop do
|
217
|
+
begin
|
218
|
+
proc, args = @tasks.synchronize do
|
219
|
+
@threads_waiting += 1
|
220
|
+
@task_enqueued.wait_while { @tasks.size <= 0 }
|
221
|
+
@threads_waiting -= 1
|
222
|
+
@tasks.shift
|
223
|
+
end
|
224
|
+
proc.call(*args)
|
225
|
+
rescue Exception => e
|
226
|
+
# Suppress Exception
|
227
|
+
ensure
|
228
|
+
@tasks.synchronize do
|
229
|
+
@cur_tasks -= 1
|
230
|
+
@task_completed.broadcast
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
243
236
|
end
|
data/tasks/test.rake
CHANGED
@@ -10,13 +10,4 @@ namespace(:test) do
|
|
10
10
|
t.warning = true
|
11
11
|
end
|
12
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
13
|
end
|
data/test/tc_work_queue.rb
CHANGED
@@ -37,6 +37,28 @@ class TC_WorkQueue < Test::Unit::TestCase
|
|
37
37
|
wq.join
|
38
38
|
assert_equal(s, "Hello #2")
|
39
39
|
end
|
40
|
+
|
41
|
+
def test_inner_enqueue
|
42
|
+
s = String.new
|
43
|
+
wq = WorkQueue.new
|
44
|
+
wq.enqueue_b do
|
45
|
+
sleep 0.01
|
46
|
+
wq.enqueue_b(s) { |str| str.replace("Hello #1") }
|
47
|
+
sleep 0.01
|
48
|
+
end
|
49
|
+
wq.join
|
50
|
+
assert_equal(s, "Hello #1")
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_threads_recycle
|
54
|
+
wq = WorkQueue.new
|
55
|
+
wq.enqueue_b { sleep 0.01 }
|
56
|
+
sleep 0.02
|
57
|
+
assert_equal(wq.cur_threads, 1)
|
58
|
+
wq.enqueue_b { sleep 0.01 }
|
59
|
+
assert_equal(wq.cur_threads, 1)
|
60
|
+
wq.join
|
61
|
+
end
|
40
62
|
|
41
63
|
def test_max_threads
|
42
64
|
assert_raise(ArgumentError) { WorkQueue.new(0) }
|
@@ -64,21 +86,10 @@ class TC_WorkQueue < Test::Unit::TestCase
|
|
64
86
|
wq.join
|
65
87
|
end
|
66
88
|
|
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
89
|
def test_stress
|
79
90
|
a = []
|
80
91
|
m = Mutex.new
|
81
|
-
wq = WorkQueue.new(100,200
|
92
|
+
wq = WorkQueue.new(100,200)
|
82
93
|
(1..1000).each do
|
83
94
|
wq.enqueue_b(a,m) { |str,mut|
|
84
95
|
sleep(0.01)
|
@@ -89,4 +100,12 @@ class TC_WorkQueue < Test::Unit::TestCase
|
|
89
100
|
assert_equal(a.size, 1000)
|
90
101
|
end
|
91
102
|
|
103
|
+
def test_kill
|
104
|
+
s = String.new
|
105
|
+
wq = WorkQueue.new
|
106
|
+
wq.enqueue_b(s) { |str| sleep 0.1; str.replace("Hello") }
|
107
|
+
wq.kill
|
108
|
+
assert_equal(s, "")
|
109
|
+
end
|
110
|
+
|
92
111
|
end
|
metadata
CHANGED
@@ -1,7 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: work_queue
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
|
4
|
+
hash: 13
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 2
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 2.0.1
|
5
11
|
platform: ruby
|
6
12
|
authors:
|
7
13
|
- Miguel Fonseca
|
@@ -9,8 +15,7 @@ autorequire:
|
|
9
15
|
bindir: bin
|
10
16
|
cert_chain: []
|
11
17
|
|
12
|
-
date:
|
13
|
-
default_executable:
|
18
|
+
date: 2011-11-21 00:00:00 Z
|
14
19
|
dependencies: []
|
15
20
|
|
16
21
|
description:
|
@@ -29,7 +34,6 @@ files:
|
|
29
34
|
- tasks/test.rake
|
30
35
|
- lib/work_queue.rb
|
31
36
|
- test/tc_work_queue.rb
|
32
|
-
has_rdoc: true
|
33
37
|
homepage: http://github.com/fmmfonseca/work_queue
|
34
38
|
licenses: []
|
35
39
|
|
@@ -42,21 +46,29 @@ rdoc_options:
|
|
42
46
|
require_paths:
|
43
47
|
- lib
|
44
48
|
required_ruby_version: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
45
50
|
requirements:
|
46
51
|
- - ">="
|
47
52
|
- !ruby/object:Gem::Version
|
53
|
+
hash: 59
|
54
|
+
segments:
|
55
|
+
- 1
|
56
|
+
- 8
|
57
|
+
- 6
|
48
58
|
version: 1.8.6
|
49
|
-
version:
|
50
59
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
60
|
+
none: false
|
51
61
|
requirements:
|
52
62
|
- - ">="
|
53
63
|
- !ruby/object:Gem::Version
|
64
|
+
hash: 3
|
65
|
+
segments:
|
66
|
+
- 0
|
54
67
|
version: "0"
|
55
|
-
version:
|
56
68
|
requirements: []
|
57
69
|
|
58
70
|
rubyforge_project:
|
59
|
-
rubygems_version: 1.
|
71
|
+
rubygems_version: 1.8.11
|
60
72
|
signing_key:
|
61
73
|
specification_version: 3
|
62
74
|
summary: A tunable work queue, designed to coordinate work between a producer and a pool of worker threads.
|