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.
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +346 -0
- data/Rakefile +26 -0
- data/benchmark/comparison.rb +103 -0
- data/benchmark/submission.rb +150 -0
- data/lib/proco.rb +112 -0
- data/lib/proco/dispatcher.rb +59 -0
- data/lib/proco/future.rb +40 -0
- data/lib/proco/logger.rb +21 -0
- data/lib/proco/mt/base.rb +65 -0
- data/lib/proco/mt/pool.rb +53 -0
- data/lib/proco/mt/threaded.rb +52 -0
- data/lib/proco/mt/worker.rb +81 -0
- data/lib/proco/queue/base.rb +58 -0
- data/lib/proco/queue/batch_queue.rb +30 -0
- data/lib/proco/queue/multi_queue.rb +30 -0
- data/lib/proco/queue/single_queue.rb +23 -0
- data/lib/proco/version.rb +3 -0
- data/proco.gemspec +24 -0
- data/test/test_mt_base.rb +40 -0
- data/test/test_mt_threaded.rb +32 -0
- data/test/test_pool.rb +20 -0
- data/test/test_proco.rb +197 -0
- data/test/test_queue.rb +105 -0
- data/test/test_worker.rb +19 -0
- metadata +144 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
class Proco
|
|
2
|
+
module MT
|
|
3
|
+
# @private
|
|
4
|
+
class Pool
|
|
5
|
+
include Proco::Logger
|
|
6
|
+
|
|
7
|
+
def initialize size, logger = nil
|
|
8
|
+
@logger = logger
|
|
9
|
+
@workers = size.times.map { |i|
|
|
10
|
+
Worker.new @logger
|
|
11
|
+
}
|
|
12
|
+
@num_workers = @workers.length
|
|
13
|
+
if @num_workers > 1
|
|
14
|
+
self.instance_eval do
|
|
15
|
+
alias assign assign_try
|
|
16
|
+
end
|
|
17
|
+
else
|
|
18
|
+
self.instance_eval do
|
|
19
|
+
alias assign assign_wait
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def assign_wait &block
|
|
25
|
+
# Optimistic randomized assignment to avoid mutex contention
|
|
26
|
+
@workers.sample.assign(&block)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def assign_try &block
|
|
30
|
+
@num_workers.times do |i|
|
|
31
|
+
ret = @workers.sample.try_assign(&block)
|
|
32
|
+
return ret if ret
|
|
33
|
+
end
|
|
34
|
+
# phew. blocking assignment
|
|
35
|
+
# debug "Failed immediate thread allocation in the 1st round (#@num_workers)"
|
|
36
|
+
assign_wait(&block)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def exit
|
|
40
|
+
@workers.each(&:exit)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def kill
|
|
44
|
+
@workers.each(&:kill)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def counter
|
|
48
|
+
@workers.map(&:counter).inject(:+)
|
|
49
|
+
end
|
|
50
|
+
end#Pool
|
|
51
|
+
end#MT
|
|
52
|
+
end#Proco
|
|
53
|
+
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
class Proco
|
|
2
|
+
module MT
|
|
3
|
+
# @private
|
|
4
|
+
module Threaded
|
|
5
|
+
include Proco::Logger
|
|
6
|
+
include Proco::MT::Base
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
super
|
|
10
|
+
@running = false
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def running?
|
|
14
|
+
@running
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def exit
|
|
18
|
+
broadcast do
|
|
19
|
+
@running = false
|
|
20
|
+
end
|
|
21
|
+
@thread.join
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def kill
|
|
25
|
+
@running = false
|
|
26
|
+
Thread.kill @thread if @thread
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def spawn &block
|
|
30
|
+
@thread = Thread.new do
|
|
31
|
+
debug "#{Thread.current} started (#{self})"
|
|
32
|
+
broadcast do
|
|
33
|
+
@running = true
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
begin
|
|
37
|
+
block.call
|
|
38
|
+
rescue Exception => e
|
|
39
|
+
error "[#{Thread.current}] #{e}"
|
|
40
|
+
raise
|
|
41
|
+
ensure
|
|
42
|
+
debug "#{Thread.current} exited (#{self})"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
wait_until { running? }
|
|
46
|
+
|
|
47
|
+
@thread
|
|
48
|
+
end
|
|
49
|
+
end#Threaded
|
|
50
|
+
end#MT
|
|
51
|
+
end#Proco
|
|
52
|
+
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
class Proco
|
|
2
|
+
module MT
|
|
3
|
+
# @private
|
|
4
|
+
class Worker
|
|
5
|
+
include Proco::MT::Threaded
|
|
6
|
+
|
|
7
|
+
attr_reader :counter
|
|
8
|
+
|
|
9
|
+
def initialize logger
|
|
10
|
+
super()
|
|
11
|
+
|
|
12
|
+
@logger = logger
|
|
13
|
+
@block = nil
|
|
14
|
+
@counter = 0
|
|
15
|
+
|
|
16
|
+
spawn do
|
|
17
|
+
work while running?
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def work
|
|
22
|
+
# Not using do_when makes the code around the task block about 10% faster
|
|
23
|
+
@mtx.lock
|
|
24
|
+
while true
|
|
25
|
+
break if @block
|
|
26
|
+
return unless running?
|
|
27
|
+
@cv.wait @mtx
|
|
28
|
+
end
|
|
29
|
+
block = @block
|
|
30
|
+
@counter += 1
|
|
31
|
+
@block = nil
|
|
32
|
+
|
|
33
|
+
# Release lock here, so that a new task can be assigned during the execution
|
|
34
|
+
# 50 -> 30
|
|
35
|
+
@cv.broadcast
|
|
36
|
+
@mtx.unlock
|
|
37
|
+
|
|
38
|
+
# Work!
|
|
39
|
+
block.call
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Blocks when working
|
|
43
|
+
def assign &block
|
|
44
|
+
#do_when(Proc.new { return unless running?; @block.nil? }) do
|
|
45
|
+
# @block = block
|
|
46
|
+
#end
|
|
47
|
+
@mtx.lock
|
|
48
|
+
while true
|
|
49
|
+
break unless @block
|
|
50
|
+
return unless running?
|
|
51
|
+
@cv.wait @mtx
|
|
52
|
+
end
|
|
53
|
+
@block = block
|
|
54
|
+
ensure
|
|
55
|
+
@cv.broadcast
|
|
56
|
+
@mtx.unlock
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Returns nil when working
|
|
60
|
+
def try_assign &block
|
|
61
|
+
#try_when(Proc.new { return unless running?; @block.nil? }) do
|
|
62
|
+
# @block = block
|
|
63
|
+
#end
|
|
64
|
+
return unless @mtx.try_lock
|
|
65
|
+
|
|
66
|
+
begin
|
|
67
|
+
return if !running? || @block
|
|
68
|
+
@block = block
|
|
69
|
+
ensure
|
|
70
|
+
@cv.broadcast
|
|
71
|
+
@mtx.unlock
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def exit
|
|
76
|
+
wait_until { @block.nil? }
|
|
77
|
+
super
|
|
78
|
+
end
|
|
79
|
+
end#Worker
|
|
80
|
+
end#MT
|
|
81
|
+
end#Proco
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
require 'thread'
|
|
2
|
+
|
|
3
|
+
class Proco
|
|
4
|
+
module Queue
|
|
5
|
+
# @private
|
|
6
|
+
class Base
|
|
7
|
+
include Proco::MT::Base
|
|
8
|
+
|
|
9
|
+
class Invalidated < Exception
|
|
10
|
+
def to_s
|
|
11
|
+
"Queue invalidated"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize size
|
|
16
|
+
super()
|
|
17
|
+
@size = size
|
|
18
|
+
@items = []
|
|
19
|
+
@valid = true
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def invalidate
|
|
23
|
+
broadcast do
|
|
24
|
+
@valid = false
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def push item
|
|
29
|
+
@mtx.lock
|
|
30
|
+
while true
|
|
31
|
+
raise Invalidated unless @valid
|
|
32
|
+
break if @items.length < @size
|
|
33
|
+
@cv.wait @mtx
|
|
34
|
+
end
|
|
35
|
+
push_impl item
|
|
36
|
+
ensure
|
|
37
|
+
@cv.broadcast
|
|
38
|
+
@mtx.unlock
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def take
|
|
42
|
+
@mtx.lock
|
|
43
|
+
while true
|
|
44
|
+
empty = @items.empty?
|
|
45
|
+
break unless empty
|
|
46
|
+
return nil unless @valid
|
|
47
|
+
@cv.wait @mtx
|
|
48
|
+
end
|
|
49
|
+
take_impl
|
|
50
|
+
ensure
|
|
51
|
+
@cv.broadcast
|
|
52
|
+
@mtx.unlock
|
|
53
|
+
end
|
|
54
|
+
end#Base
|
|
55
|
+
end#Queue
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require 'thread'
|
|
2
|
+
|
|
3
|
+
class Proco
|
|
4
|
+
module Queue
|
|
5
|
+
# @private
|
|
6
|
+
class BatchQueue < Proco::Queue::Base
|
|
7
|
+
def initialize size, batch_size
|
|
8
|
+
super size
|
|
9
|
+
@futures = []
|
|
10
|
+
@batch_size = batch_size
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def push_impl item
|
|
14
|
+
@items << item
|
|
15
|
+
if @items.length % @batch_size == 1
|
|
16
|
+
@futures << Future.new
|
|
17
|
+
end
|
|
18
|
+
@futures.last
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def take_impl
|
|
22
|
+
items = @items[0, @batch_size]
|
|
23
|
+
@items = @items[@batch_size..-1] || []
|
|
24
|
+
|
|
25
|
+
[@futures.shift, items]
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require 'thread'
|
|
2
|
+
|
|
3
|
+
class Proco
|
|
4
|
+
module Queue
|
|
5
|
+
# @private
|
|
6
|
+
class MultiQueue < Proco::Queue::Base
|
|
7
|
+
def initialize size
|
|
8
|
+
super
|
|
9
|
+
@future = Future.new
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def push_impl item
|
|
13
|
+
@items << item
|
|
14
|
+
@future
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def take_impl
|
|
18
|
+
items = @items
|
|
19
|
+
ret = [@future, items]
|
|
20
|
+
|
|
21
|
+
# Reset vars
|
|
22
|
+
@items = []
|
|
23
|
+
@future = Future.new
|
|
24
|
+
|
|
25
|
+
ret
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
require 'thread'
|
|
2
|
+
|
|
3
|
+
class Proco
|
|
4
|
+
module Queue
|
|
5
|
+
# @private
|
|
6
|
+
class SingleQueue < Proco::Queue::Base
|
|
7
|
+
def initialize size
|
|
8
|
+
super
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def push_impl item
|
|
12
|
+
future = Future.new
|
|
13
|
+
@items << [future, item]
|
|
14
|
+
future
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def take_impl
|
|
18
|
+
@items.shift
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
data/proco.gemspec
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
|
+
require 'proco/version'
|
|
5
|
+
|
|
6
|
+
Gem::Specification.new do |gem|
|
|
7
|
+
gem.name = "proco"
|
|
8
|
+
gem.version = Proco::VERSION
|
|
9
|
+
gem.authors = ["Junegunn Choi"]
|
|
10
|
+
gem.email = ["junegunn.c@gmail.com"]
|
|
11
|
+
gem.description = %q{A lightweight asynchronous task executor service designed for efficient batch processing}
|
|
12
|
+
gem.summary = %q{A lightweight asynchronous task executor service designed for efficient batch processing}
|
|
13
|
+
gem.homepage = "https://github.com/junegunn/proco"
|
|
14
|
+
|
|
15
|
+
gem.files = `git ls-files`.split($/).reject { |f| f =~ %r[^viz/] }
|
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
|
18
|
+
gem.require_paths = ["lib"]
|
|
19
|
+
|
|
20
|
+
gem.add_runtime_dependency 'lps', '~> 0.1.3'
|
|
21
|
+
gem.add_runtime_dependency 'option_initializer', '~> 1.2.0'
|
|
22
|
+
gem.add_development_dependency 'minitest'
|
|
23
|
+
gem.add_development_dependency 'parallelize'
|
|
24
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
$VERBOSE = true
|
|
2
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) if $0 == __FILE__
|
|
3
|
+
require 'rubygems'
|
|
4
|
+
require 'minitest/autorun'
|
|
5
|
+
require 'proco'
|
|
6
|
+
|
|
7
|
+
class MTBaseSub
|
|
8
|
+
include Proco::MT::Base
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
super
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def ok dur
|
|
15
|
+
a = false
|
|
16
|
+
t = Thread.new { sleep dur; a = :ok; self.signal }
|
|
17
|
+
do_when(proc { a }) do
|
|
18
|
+
a
|
|
19
|
+
end.tap { t.join }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def not_ok
|
|
23
|
+
# Proc.new instead of proc (1.8 compat)
|
|
24
|
+
do_when(Proc.new { return :return }) do
|
|
25
|
+
:anything
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class TestMTBase < MiniTest::Unit::TestCase
|
|
31
|
+
def test_mt_base
|
|
32
|
+
t = MTBaseSub.new
|
|
33
|
+
st = Time.now
|
|
34
|
+
assert_equal :ok, t.ok(0.1)
|
|
35
|
+
assert Time.now - st >= 0.1 # FIXME
|
|
36
|
+
|
|
37
|
+
assert_equal :return, t.not_ok
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
$VERBOSE = true
|
|
2
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) if $0 == __FILE__
|
|
3
|
+
require 'rubygems'
|
|
4
|
+
require 'minitest/autorun'
|
|
5
|
+
require 'proco'
|
|
6
|
+
|
|
7
|
+
class MTThreadedSub
|
|
8
|
+
include Proco::MT::Threaded
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
super
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class TestMTThreaded < MiniTest::Unit::TestCase
|
|
16
|
+
def test_mt_base
|
|
17
|
+
t = MTThreadedSub.new
|
|
18
|
+
assert_equal false, t.running?
|
|
19
|
+
status = nil
|
|
20
|
+
t.spawn do
|
|
21
|
+
while t.running?
|
|
22
|
+
sleep 0.01
|
|
23
|
+
end
|
|
24
|
+
status = :done
|
|
25
|
+
end
|
|
26
|
+
assert_equal true, t.running?
|
|
27
|
+
t.exit
|
|
28
|
+
assert_equal :done, status
|
|
29
|
+
assert_equal false, t.running?
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
data/test/test_pool.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
$VERBOSE = true
|
|
2
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) if $0 == __FILE__
|
|
3
|
+
require 'rubygems'
|
|
4
|
+
require 'minitest/autorun'
|
|
5
|
+
require 'proco'
|
|
6
|
+
|
|
7
|
+
class TestPool < MiniTest::Unit::TestCase
|
|
8
|
+
def test_pool
|
|
9
|
+
pool = Proco::MT::Pool.new(8)
|
|
10
|
+
cnt = 0
|
|
11
|
+
mtx = Mutex.new
|
|
12
|
+
1000.times do
|
|
13
|
+
pool.assign { mtx.synchronize { cnt += 3 } }
|
|
14
|
+
end
|
|
15
|
+
pool.exit
|
|
16
|
+
assert_equal 1000, pool.counter
|
|
17
|
+
assert_equal 3000, cnt
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|