proco 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .rbx
4
+ .bundle
5
+ .config
6
+ .yardoc
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in grouper.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Junegunn Choi
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,346 @@
1
+ Proco
2
+ =====
3
+
4
+ Proco is a lightweight asynchronous task executor service with a thread pool
5
+ especially designed for efficient batch processing of multiple data items.
6
+
7
+ ### What Proco is
8
+ - Lightweight, easy-to-use building block for concurrency in Ruby
9
+ - High-throughput reactor for relatively simple, short-lived tasks
10
+ - Proco can dispatch hundreds of thousands of items per second
11
+
12
+ ### What Proco is not
13
+ - Omnipotent "does-it-all" super gem
14
+ - Background task schedulers like Resque or DelayedJob
15
+
16
+ A quick example
17
+ ---------------
18
+
19
+ ```ruby
20
+ require 'proco'
21
+
22
+ proco = Proco.interval(0.1). # Runs every 0.1 second
23
+ threads(4). # 4 threads processing items every interval
24
+ batch(true).new # Enables batch processing mode
25
+
26
+ proco.start do |items|
27
+ # Batch-process items and return something
28
+ # ...
29
+ end
30
+
31
+ # Synchronous submit
32
+ result = proco.submit rand(1000)
33
+
34
+ # Asynchronous(!) submit (can block if the queue is full)
35
+ future = proco.submit! rand(1000)
36
+
37
+ # Wait until the batch containing the item is processed
38
+ # (Commit notification)
39
+ result = future.get
40
+
41
+ # Process remaining submissions and terminate threads
42
+ proco.exit
43
+ ```
44
+
45
+ Requirements
46
+ ------------
47
+
48
+ Proco requires Ruby 1.8 or higher. Tested on MRI 1.8.7/1.9.3/2.0.0, and JRuby 1.7.3.
49
+
50
+ Installation
51
+ ------------
52
+
53
+ gem install proco
54
+
55
+ Architecture
56
+ ------------
57
+
58
+ ![Basic producer-consumer configuration](https://github.com/junegunn/proco/raw/master/viz/producer-consumer.png)
59
+
60
+ Proco is based on the traditional [producer-consumer model](http://en.wikipedia.org/wiki/Producer-consumer_problem)
61
+ (hence the name *ProCo*).
62
+
63
+ - Mutliple clients simultaneously submits (*produces*) items to be processed.
64
+ - A client can asynchronously submit an item and optionally wait for its completion.
65
+ - Executor threads in the thread pool process (*consumes*) items concurrently.
66
+ - A submitted item is first put into a fixed sized queue.
67
+ - A queue has its own dedicated dispatcher thread.
68
+ - Each item in the queue is taken out by the dispatcher and assigned to one of the executor threads.
69
+ - Assignments can be done periodically at certain interval, where multiple items are assigned at once for batch processing
70
+ - In a highly concurrent environment, event loop of the dispatcher thread can become the bottleneck.
71
+ - Thus, Proco can be configured to have multiple queues and dispatcher threads
72
+ - However, for strict serializability (FCFS), you should just have a single queue and a single executor thread (default).
73
+
74
+ ### Proco with a single queue and thread
75
+
76
+ ![Default Proco configuration](https://github.com/junegunn/proco/raw/master/viz/proco-6-1-1.png)
77
+
78
+ ```ruby
79
+ proco = Proco.new
80
+ ```
81
+
82
+ ### Proco with multiple queues
83
+
84
+ ![Proco with multiple queues](https://github.com/junegunn/proco/raw/master/viz/proco-6-4-5.png)
85
+
86
+ ```ruby
87
+ proco = Proco.threads(5).queues(4).new
88
+ ```
89
+
90
+ Batch processing
91
+ ----------------
92
+
93
+ Sometimes it really helps to process multiple items in batch instead of one at a time.
94
+
95
+ Notable examples includes:
96
+ - buffered disk I/O in Kernel
97
+ - consolidated e-mail notification
98
+ - database batch updates
99
+ - group commit of database transactions
100
+
101
+ In this scheme, we don't process a request as soon as it arrives,
102
+ but wait a little while hoping that we receive more requests as well,
103
+ so we can process them together with minimal amortized latency.
104
+
105
+ It's a pretty common pattern, that most developers will be writing similar scenarios
106
+ one way or another at some point. So *why don't we make the pattern reusable*?
107
+
108
+ Proco was designed with this goal in mind.
109
+ As described above, item assignments can be done periodically at the specified interval,
110
+ so that multiple items are piled up in the queue between assignments,
111
+ and then given to one of the executor threads at once in batch.
112
+
113
+ ```ruby
114
+ # Assigns items in batch every second
115
+ proco = Proco.interval(1).batch(true).new
116
+ ```
117
+
118
+ Thread pool
119
+ -----------
120
+
121
+ Proco implements a pool of concurrently running executor threads.
122
+ If you're running CRuby, multi-threading only makes sense if your task involves blocking I/O operations.
123
+ On JRuby or Rubinius, executor threads will run in parallel and efficiently utilize multiple cores.
124
+
125
+ ```ruby
126
+ # Proco with 8 executor threads
127
+ proco = Proco.threads(8).new
128
+ ```
129
+
130
+ Proco API
131
+ ---------
132
+
133
+ API of Proco is pretty minimal. The following flowchart summarizes the supported operations.
134
+
135
+ ![Life of Proco](https://github.com/junegunn/proco/raw/master/viz/proco-lifecycle.png)
136
+
137
+ ### Initialization
138
+
139
+ A Proco object can be initialized by chaining the following
140
+ [option initializer](https://github.com/junegunn/option_initializer) methods.
141
+
142
+ | Option | Type | Description |
143
+ |------------|---------|------------------------------------------------|
144
+ | threads | Fixnum | number of threads in the thread pool |
145
+ | queues | Fixnum | number of concurrent queues |
146
+ | queue_size | Fixnum | size of each queue |
147
+ | interval | Numeric | dispatcher interval for batch processing |
148
+ | batch | Boolean | enables batch processing mode |
149
+ | batch_size | Fixnum | number of maximum items to be assigned at once |
150
+ | logger | Logger | logger instance for debug logs |
151
+
152
+ ```ruby
153
+ # Initialization with method chaining
154
+ proco = Proco.interval(0.1).threads(8).queues(4).queue_size(100).batch(true).batch_size(10).new
155
+
156
+ # Traditional initialization with options hash is also allowed
157
+ proco = Proco.new(
158
+ interval: 0.1,
159
+ threads: 8,
160
+ queues: 4,
161
+ queue_size: 100,
162
+ batch: true,
163
+ batch_size: 10)
164
+ ```
165
+
166
+ ### Starting
167
+
168
+ ```ruby
169
+ # Regular Proco
170
+ proco = Proco.new
171
+ proco.start do |item|
172
+ # code for single item
173
+ end
174
+
175
+ # Proco in batch mode
176
+ proco = Proco.batch(true).new
177
+ proco.start do |items|
178
+ # code for multiple items
179
+ end
180
+ ```
181
+
182
+ ### Submitting items
183
+
184
+ ```ruby
185
+ # Synchronous submission
186
+ proco.submit 100
187
+
188
+ # Asynchronous(1) submission
189
+ future = proco.submit! 100
190
+ value = future.get
191
+ ```
192
+
193
+ ### Quitting
194
+
195
+ ```ruby
196
+ # Graceful shutdown
197
+ proco.exit
198
+
199
+ # Immediately kills all running threads
200
+ proco.kill
201
+ ```
202
+
203
+ Benchmarks
204
+ ----------
205
+
206
+ The purpose of the benchmarks shown here is not to present absolute
207
+ measurements of performance but to give you a general idea of how Proco should
208
+ be configured under various workloads of different characteristics.
209
+
210
+ The following benchmark results were gathered on an 8-core system with JRuby 1.7.3.
211
+
212
+ ### Modeling CPU-intensive task
213
+
214
+ - The task does not involve any blocking I/O
215
+ - A fixed amount of CPU time is required for each item
216
+ - There's little benefit of batch processing as the total amount of work is just the same
217
+
218
+ #### Task definition
219
+
220
+ ```ruby
221
+ task = lambda do |item|
222
+ (1..10000).inject(:+)
223
+ end
224
+
225
+ # Total amount of work is just the same
226
+ batch_task = lambda do |items|
227
+ items.each do
228
+ (1..10000).inject(:+)
229
+ end
230
+ end
231
+ ```
232
+
233
+ #### Result
234
+
235
+ ```
236
+ : Elapsed time
237
+ loop : *********************************************************
238
+ Proco.new : ************************************************************
239
+ Proco.threads(2).queues(1).new : *******************************
240
+ Proco.threads(2).queues(1).batch(true).new : ***********************************
241
+ Proco.threads(2).queues(4).new : *******************************
242
+ Proco.threads(2).queues(4).batch(true).new : ********************************
243
+ Proco.threads(4).queues(1).new : ****************
244
+ Proco.threads(4).queues(1).batch(true).new : ************************
245
+ Proco.threads(4).queues(4).new : ****************
246
+ Proco.threads(4).queues(4).batch(true).new : ********************
247
+ Proco.threads(8).queues(1).new : *********
248
+ Proco.threads(8).queues(1).batch(true).new : ******************
249
+ Proco.threads(8).queues(4).new : *********
250
+ Proco.threads(8).queues(4).batch(true).new : *************
251
+ ```
252
+
253
+ ##### Analysis
254
+
255
+ - Proco with default configuration is slightly slower than simple loop due to thread coordination overhead
256
+ - As we increase the number of threads performance increases as we utilize more CPU cores
257
+ - Dispatcher thread is not the bottleneck. Increasing the number of queues and their dispatcher threads doesn't do any good.
258
+ - Batch mode takes longer as the tasks are not uniformly distributed among threads
259
+ - We can set `batch_size` to limit the maximum number of items in a batch
260
+
261
+ ##### Result with batch_size = 100
262
+
263
+ ```
264
+ proco = Proco.batch_size(100)
265
+ : Elapsed time
266
+ loop : *********************************************************
267
+ proco.new : ************************************************************
268
+ proco.threads(2).queues(1).new : *******************************
269
+ proco.threads(2).queues(1).batch(true).new : *******************************
270
+ proco.threads(2).queues(4).new : *******************************
271
+ proco.threads(2).queues(4).batch(true).new : ******************************
272
+ proco.threads(4).queues(1).new : ****************
273
+ proco.threads(4).queues(1).batch(true).new : ***************
274
+ proco.threads(4).queues(4).new : ****************
275
+ proco.threads(4).queues(4).batch(true).new : ***************
276
+ proco.threads(8).queues(1).new : *********
277
+ proco.threads(8).queues(1).batch(true).new : *********
278
+ proco.threads(8).queues(4).new : *********
279
+ proco.threads(8).queues(4).batch(true).new : ********
280
+ ```
281
+
282
+ ### Modeling direct I/O on a single disk
283
+
284
+ - We're bypassing write buffer of the Kernel
285
+ - Time required to write data on disk is dominated by the seek time
286
+ - Let's assume seek time of our disk is 10ms, and data transfer rate, 50MB/sec
287
+ - Each request writes 50kB amount of data
288
+ - As we have only one disk, writes cannot occur concurrently
289
+
290
+ #### Task definition
291
+
292
+ ```ruby
293
+ # Mutex for simulating exclusive disk access
294
+ mtx = Mutex.new
295
+
296
+ task = lambda do |item|
297
+ mtx.synchronize do
298
+ # Seek time: 0.01 sec
299
+ # Transfer time: 50kB / 50MB/sec = 0.001 sec
300
+ sleep 0.01 + 0.001
301
+ end
302
+ end
303
+
304
+ # Let's say it makes sense to group multiple writes into a single I/O operation
305
+ batch_task = lambda do |items|
306
+ mtx.synchronize do
307
+ # Seek time: 0.01 sec
308
+ # Transfer time: n * (50kB / 50MB/sec) = n * 0.001 sec
309
+ sleep 0.01 + items.length * 0.001
310
+ end
311
+ end
312
+ ```
313
+
314
+ #### Result
315
+
316
+ ```
317
+ loop : ***********************************************************
318
+ Proco.new : ***********************************************************
319
+ Proco.threads(2).queues(1).new : ***********************************************************
320
+ Proco.threads(2).queues(1).batch(true).new : ****
321
+ Proco.threads(2).queues(4).new : ***********************************************************
322
+ Proco.threads(2).queues(4).batch(true).new : *****
323
+ Proco.threads(4).queues(1).new : ***********************************************************
324
+ Proco.threads(4).queues(1).batch(true).new : ****
325
+ Proco.threads(4).queues(4).new : ***********************************************************
326
+ Proco.threads(4).queues(4).batch(true).new : *****
327
+ Proco.threads(8).queues(1).new : ************************************************************
328
+ Proco.threads(8).queues(1).batch(true).new : ****
329
+ Proco.threads(8).queues(4).new : ***********************************************************
330
+ Proco.threads(8).queues(4).batch(true).new : ****
331
+ ```
332
+
333
+
334
+ ##### Analysis
335
+
336
+ - The number of threads, queues or dispather threads, none of them matters
337
+ - Batch mode shows much better performance
338
+
339
+ Contributing
340
+ ------------
341
+
342
+ 1. Fork it
343
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
344
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
345
+ 4. Push to the branch (`git push origin my-new-feature`)
346
+ 5. Create new Pull Request
@@ -0,0 +1,26 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rake/testtask'
3
+ require 'fileutils'
4
+
5
+ Rake::TestTask.new(:test) do |test|
6
+ test.libs << 'lib' << 'test'
7
+ test.pattern = 'test/**/test_*.rb'
8
+ test.verbose = true
9
+ end
10
+
11
+ task :viz do
12
+ FileUtils.chdir(File.expand_path('..', __FILE__))
13
+
14
+ [ [6, 1, 5],
15
+ [6, 1, 1],
16
+ [6, 4, 5] ].each do |vars|
17
+ c, q, t = vars
18
+ ENV['C'], ENV['Q'], ENV['T'] = vars.map(&:to_s)
19
+ file = "viz/proco-#{vars.join '-'}.png"
20
+ system %[erb viz/proco.dot.erb | dot -Tpng -o #{file} && open #{file}]
21
+ end
22
+
23
+ %w[producer-consumer proco-lifecycle].each do |file|
24
+ system %[dot -Tpng -o viz/#{file}.png viz/#{file}.dot && open viz/#{file}.png]
25
+ end
26
+ end
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $VERBOSE = true
4
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
5
+ require 'proco'
6
+ require 'benchmark'
7
+ require 'parallelize'
8
+ require 'logger'
9
+
10
+ logger = Logger.new($stdout)
11
+
12
+ [:cpu, :directio].each do |mode|
13
+ if mode == :cpu
14
+ times = 20000
15
+ # CPU Intensive task
16
+ task = lambda do |item|
17
+ (1..10000).inject(:+)
18
+ end
19
+
20
+ btask = lambda do |items|
21
+ items.each do
22
+ (1..10000).inject(:+)
23
+ end
24
+ end
25
+ else
26
+ mtx = Mutex.new
27
+
28
+ times = 1000
29
+ task = lambda do |item|
30
+ mtx.synchronize do
31
+ sleep 0.01 + 0.001
32
+ end
33
+ end
34
+
35
+ btask = lambda do |items|
36
+ mtx.synchronize do
37
+ sleep 0.01 + 0.001 * items.length
38
+ end
39
+ end
40
+ end
41
+
42
+ result = Benchmark.bm(45) do |x|
43
+ x.report("loop") do
44
+ times.times do |i|
45
+ task.call i
46
+ end
47
+ end
48
+
49
+ x.report('Proco.new') do
50
+ proco = Proco.new
51
+ proco.start do |i|
52
+ task.call i
53
+ end
54
+ times.times do |i|
55
+ proco.submit! i
56
+ end
57
+ proco.exit
58
+ end
59
+
60
+ [2, 4, 8].each do |threads|
61
+ x.report("parallelize(#{threads})") do
62
+ parallelize(threads) do
63
+ (times / threads).times do |i|
64
+ task.call i
65
+ end
66
+ end
67
+ end
68
+
69
+ [1, 4].each do |queues|
70
+ x.report("Proco.threads(#{threads}).queues(#{queues}).new") do
71
+ proco = Proco.queues(queues).threads(threads).new
72
+ proco.start do |i|
73
+ task.call i
74
+ end
75
+ times.times do |i|
76
+ proco.submit! i
77
+ end
78
+ proco.exit
79
+ end
80
+
81
+ x.report("Proco.threads(#{threads}).queues(#{queues}).batch(true).new") do
82
+ proco = Proco.queues(queues).threads(threads).batch(true).new
83
+ proco.start do |items|
84
+ btask.call items
85
+ end
86
+ times.times do |i|
87
+ proco.submit! i
88
+ end
89
+ proco.exit
90
+ end
91
+ end
92
+ end
93
+ end
94
+
95
+ data = Hash[ result.map { |r| [r.label, r.real] } ]
96
+ mlen = data.keys.map(&:length).max
97
+ mval = data.values.max
98
+ width = 40
99
+ data.each do |k, v|
100
+ puts k.ljust(mlen) + ' : ' + '*' * (width * (v / mval)).to_i
101
+ end
102
+ puts
103
+ end