proco 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,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