concurrent-ruby 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +48 -1
- data/lib/concurrent.rb +8 -1
- data/lib/concurrent/agent.rb +19 -40
- data/lib/concurrent/cached_thread_pool.rb +10 -11
- data/lib/concurrent/defer.rb +8 -12
- data/lib/concurrent/executor.rb +95 -0
- data/lib/concurrent/fixed_thread_pool.rb +12 -6
- data/lib/concurrent/functions.rb +120 -0
- data/lib/concurrent/future.rb +8 -20
- data/lib/concurrent/global_thread_pool.rb +13 -0
- data/lib/concurrent/goroutine.rb +5 -1
- data/lib/concurrent/null_thread_pool.rb +22 -0
- data/lib/concurrent/obligation.rb +10 -64
- data/lib/concurrent/promise.rb +38 -60
- data/lib/concurrent/reactor.rb +166 -0
- data/lib/concurrent/reactor/drb_async_demux.rb +83 -0
- data/lib/concurrent/reactor/tcp_sync_demux.rb +131 -0
- data/lib/concurrent/supervisor.rb +100 -0
- data/lib/concurrent/thread_pool.rb +16 -5
- data/lib/concurrent/utilities.rb +8 -0
- data/lib/concurrent/version.rb +1 -1
- data/md/defer.md +4 -4
- data/md/executor.md +187 -0
- data/md/promise.md +2 -0
- data/md/thread_pool.md +27 -0
- data/spec/concurrent/agent_spec.rb +8 -27
- data/spec/concurrent/cached_thread_pool_spec.rb +14 -1
- data/spec/concurrent/defer_spec.rb +17 -21
- data/spec/concurrent/event_machine_defer_proxy_spec.rb +159 -149
- data/spec/concurrent/executor_spec.rb +200 -0
- data/spec/concurrent/fixed_thread_pool_spec.rb +2 -3
- data/spec/concurrent/functions_spec.rb +217 -0
- data/spec/concurrent/future_spec.rb +4 -11
- data/spec/concurrent/global_thread_pool_spec.rb +38 -0
- data/spec/concurrent/goroutine_spec.rb +15 -0
- data/spec/concurrent/null_thread_pool_spec.rb +54 -0
- data/spec/concurrent/obligation_shared.rb +127 -116
- data/spec/concurrent/promise_spec.rb +16 -14
- data/spec/concurrent/reactor/drb_async_demux_spec.rb +196 -0
- data/spec/concurrent/reactor/tcp_sync_demux_spec.rb +410 -0
- data/spec/concurrent/reactor_spec.rb +364 -0
- data/spec/concurrent/supervisor_spec.rb +258 -0
- data/spec/concurrent/thread_pool_shared.rb +156 -161
- data/spec/concurrent/utilities_spec.rb +30 -1
- data/spec/spec_helper.rb +13 -0
- metadata +38 -9
@@ -0,0 +1,166 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'functional'
|
3
|
+
require 'concurrent/supervisor'
|
4
|
+
|
5
|
+
behavior_info(:sync_event_demux,
|
6
|
+
run: 0,
|
7
|
+
stop: 0,
|
8
|
+
running?: 0,
|
9
|
+
accept: 0,
|
10
|
+
respond: 2)
|
11
|
+
|
12
|
+
behavior_info(:async_event_demux,
|
13
|
+
run: 0,
|
14
|
+
stop: 0,
|
15
|
+
running?: 0,
|
16
|
+
set_reactor: 1)
|
17
|
+
|
18
|
+
behavior_info(:demux_reactor,
|
19
|
+
handle: -2)
|
20
|
+
|
21
|
+
module Concurrent
|
22
|
+
|
23
|
+
class Reactor
|
24
|
+
|
25
|
+
behavior(:demux_reactor)
|
26
|
+
behavior(:runnable)
|
27
|
+
|
28
|
+
RESERVED_EVENTS = [ :stop ]
|
29
|
+
|
30
|
+
EventContext = Struct.new(:event, :args, :callback)
|
31
|
+
|
32
|
+
def initialize(demux = nil)
|
33
|
+
@demux = demux
|
34
|
+
if @demux.nil? || @demux.behaves_as?(:async_event_demux)
|
35
|
+
@sync = false
|
36
|
+
@queue = Queue.new
|
37
|
+
@demux.set_reactor(self) unless @demux.nil?
|
38
|
+
elsif @demux.behaves_as?(:sync_event_demux)
|
39
|
+
@sync = true
|
40
|
+
else
|
41
|
+
raise ArgumentError.new("invalid event demultiplexer '#{@demux}'")
|
42
|
+
end
|
43
|
+
|
44
|
+
@running = false
|
45
|
+
@handlers = Hash.new
|
46
|
+
@mutex = Mutex.new
|
47
|
+
end
|
48
|
+
|
49
|
+
def running?
|
50
|
+
return @running
|
51
|
+
end
|
52
|
+
|
53
|
+
def add_handler(event, &block)
|
54
|
+
raise ArgumentError.new('no block given') unless block_given?
|
55
|
+
event = event.to_sym
|
56
|
+
raise ArgumentError.new("'#{event}' is a reserved event") if RESERVED_EVENTS.include?(event)
|
57
|
+
@mutex.synchronize {
|
58
|
+
@handlers[event] = block
|
59
|
+
}
|
60
|
+
return true
|
61
|
+
end
|
62
|
+
|
63
|
+
def remove_handler(event)
|
64
|
+
handler = @mutex.synchronize {
|
65
|
+
@handlers.delete(event.to_sym)
|
66
|
+
}
|
67
|
+
return ! handler.nil?
|
68
|
+
end
|
69
|
+
|
70
|
+
def stop_on_signal(*signals)
|
71
|
+
signals.each{|signal| Signal.trap(signal){ Thread.new{ self.stop }}}
|
72
|
+
end
|
73
|
+
|
74
|
+
def handle(event, *args)
|
75
|
+
raise NotImplementedError.new("demultiplexer '#{@demux.class}' is synchronous") if @sync
|
76
|
+
return [:stopped, 'reactor not running'] unless running?
|
77
|
+
context = EventContext.new(event.to_sym, args.dup, Queue.new)
|
78
|
+
@queue.push(context)
|
79
|
+
return context.callback.pop
|
80
|
+
end
|
81
|
+
|
82
|
+
def run
|
83
|
+
raise StandardError.new('already running') if self.running?
|
84
|
+
@sync ? (@running = true; run_sync) : (@running = true; run_async)
|
85
|
+
end
|
86
|
+
alias_method :run, :run
|
87
|
+
|
88
|
+
def stop
|
89
|
+
return true unless self.running?
|
90
|
+
if @sync
|
91
|
+
@demux.stop
|
92
|
+
else
|
93
|
+
@queue.push(:stop)
|
94
|
+
end
|
95
|
+
return true
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
def handle_event(context)
|
101
|
+
raise ArgumentError.new('no block given') unless block_given?
|
102
|
+
|
103
|
+
handler = @mutex.synchronize {
|
104
|
+
@handlers[context.event]
|
105
|
+
}
|
106
|
+
|
107
|
+
if handler.nil?
|
108
|
+
response = yield(:noop, "'#{context.event}' handler not found")
|
109
|
+
else
|
110
|
+
begin
|
111
|
+
result = handler.call(*context.args)
|
112
|
+
response = yield(:ok, result)
|
113
|
+
rescue Exception => ex
|
114
|
+
response = yield(:ex, ex)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
return response
|
119
|
+
end
|
120
|
+
|
121
|
+
def finalize_stop
|
122
|
+
@mutex.synchronize do
|
123
|
+
@running = false
|
124
|
+
@demux.stop unless @demux.nil?
|
125
|
+
@demux = nil
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def run_sync
|
130
|
+
@demux.run
|
131
|
+
|
132
|
+
loop do
|
133
|
+
break unless @demux.running?
|
134
|
+
context = @demux.accept
|
135
|
+
begin
|
136
|
+
if context.nil?
|
137
|
+
@demux.stop
|
138
|
+
else
|
139
|
+
response = handle_event(context) do |result, message|
|
140
|
+
[result, message]
|
141
|
+
end
|
142
|
+
@demux.respond(*response)
|
143
|
+
end
|
144
|
+
rescue Exception => ex
|
145
|
+
@demux.respond(:abend, ex)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
finalize_stop
|
150
|
+
end
|
151
|
+
|
152
|
+
def run_async
|
153
|
+
@demux.run unless @demux.nil?
|
154
|
+
|
155
|
+
loop do
|
156
|
+
context = @queue.pop
|
157
|
+
break if context == :stop
|
158
|
+
handle_event(context) do |result, message|
|
159
|
+
context.callback.push([result, message])
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
finalize_stop
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'drb/drb'
|
2
|
+
require 'drb/acl'
|
3
|
+
require 'functional'
|
4
|
+
require 'concurrent/reactor'
|
5
|
+
require 'concurrent/supervisor'
|
6
|
+
|
7
|
+
module Concurrent
|
8
|
+
class Reactor
|
9
|
+
|
10
|
+
class DRbAsyncDemux
|
11
|
+
|
12
|
+
behavior(:async_event_demux)
|
13
|
+
|
14
|
+
DEFAULT_URI = 'druby://localhost:12345'
|
15
|
+
DEFAULT_ACL = %w{deny all allow 127.0.0.1}
|
16
|
+
|
17
|
+
attr_reader :uri
|
18
|
+
attr_reader :acl
|
19
|
+
|
20
|
+
def initialize(opts = {})
|
21
|
+
@uri = opts[:uri] || DEFAULT_URI
|
22
|
+
@acl = ACL.new(opts[:acl] || DEFAULT_ACL)
|
23
|
+
end
|
24
|
+
|
25
|
+
def set_reactor(reactor)
|
26
|
+
raise ArgumentError.new('invalid reactor') unless reactor.behaves_as?(:demux_reactor)
|
27
|
+
@reactor = reactor
|
28
|
+
end
|
29
|
+
|
30
|
+
def run
|
31
|
+
raise StandardError.new('already running') if running?
|
32
|
+
DRb.install_acl(@acl)
|
33
|
+
@service = DRb.start_service(@uri, Demultiplexer.new(@reactor))
|
34
|
+
end
|
35
|
+
|
36
|
+
def stop
|
37
|
+
@service = DRb.stop_service
|
38
|
+
end
|
39
|
+
|
40
|
+
def running?
|
41
|
+
return ! @service.nil?
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
class Demultiplexer
|
47
|
+
|
48
|
+
def initialize(reactor)
|
49
|
+
@reactor = reactor
|
50
|
+
end
|
51
|
+
|
52
|
+
Concurrent::Reactor::RESERVED_EVENTS.each do |event|
|
53
|
+
define_method(event){|*args| false }
|
54
|
+
end
|
55
|
+
|
56
|
+
def method_missing(method, *args, &block)
|
57
|
+
(class << self; self; end).class_eval do
|
58
|
+
define_method(method) do |*args|
|
59
|
+
begin
|
60
|
+
result = @reactor.handle(method, *args)
|
61
|
+
rescue Exception => ex
|
62
|
+
raise DRb::DRbRemoteError.new(ex)
|
63
|
+
end
|
64
|
+
case result.first
|
65
|
+
when :ok
|
66
|
+
return result.last
|
67
|
+
when :ex
|
68
|
+
raise result.last
|
69
|
+
when :noop
|
70
|
+
raise NoMethodError.new("undefined method '#{method}' for #{self}")
|
71
|
+
else
|
72
|
+
raise DRb::DRbError.new("unexpected response from method '#{method}'")
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
self.send(method, *args)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
DRbAsyncDemultiplexer = DRbAsyncDemux
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'drb/acl'
|
3
|
+
require 'functional'
|
4
|
+
require 'concurrent/reactor'
|
5
|
+
require 'concurrent/supervisor'
|
6
|
+
|
7
|
+
module Concurrent
|
8
|
+
class Reactor
|
9
|
+
|
10
|
+
class TcpSyncDemux
|
11
|
+
|
12
|
+
behavior(:sync_event_demux)
|
13
|
+
|
14
|
+
DEFAULT_HOST = '127.0.0.1'
|
15
|
+
DEFAULT_PORT = 12345
|
16
|
+
DEFAULT_ACL = %w{deny all allow 127.0.0.1}
|
17
|
+
|
18
|
+
attr_reader :host
|
19
|
+
attr_reader :port
|
20
|
+
attr_reader :acl
|
21
|
+
|
22
|
+
def initialize(opts = {})
|
23
|
+
@host = opts[:host] || DEFAULT_HOST
|
24
|
+
@port = opts[:port] || DEFAULT_PORT
|
25
|
+
@acl = ACL.new(opts[:acl] || DEFAULT_ACL)
|
26
|
+
end
|
27
|
+
|
28
|
+
def run
|
29
|
+
raise StandardError.new('already running') if running?
|
30
|
+
begin
|
31
|
+
@server = TCPServer.new(@host, @port)
|
32
|
+
return true
|
33
|
+
rescue Exception => ex
|
34
|
+
return false
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def stop
|
39
|
+
begin
|
40
|
+
@socket.close unless @socket.nil?
|
41
|
+
rescue Exception => ex
|
42
|
+
# suppress
|
43
|
+
end
|
44
|
+
|
45
|
+
begin
|
46
|
+
@server.close unless @server.nil?
|
47
|
+
rescue Exception => ex
|
48
|
+
# suppress
|
49
|
+
end
|
50
|
+
|
51
|
+
@server = @socket = nil
|
52
|
+
return true
|
53
|
+
end
|
54
|
+
|
55
|
+
def reset
|
56
|
+
stop
|
57
|
+
sleep(1)
|
58
|
+
run
|
59
|
+
end
|
60
|
+
|
61
|
+
def running?
|
62
|
+
return ! @server.nil?
|
63
|
+
end
|
64
|
+
|
65
|
+
def accept
|
66
|
+
@socket = @server.accept if @socket.nil?
|
67
|
+
return nil unless @acl.allow_socket?(@socket)
|
68
|
+
event, args = get_message(@socket)
|
69
|
+
return nil if event.nil?
|
70
|
+
return Reactor::EventContext.new(event, args)
|
71
|
+
rescue Exception => ex
|
72
|
+
reset
|
73
|
+
return nil
|
74
|
+
end
|
75
|
+
|
76
|
+
def respond(result, message)
|
77
|
+
return nil if @socket.nil?
|
78
|
+
@socket.puts(format_message(result, message))
|
79
|
+
rescue Exception => ex
|
80
|
+
reset
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.format_message(event, *args)
|
84
|
+
event = event.to_s.strip
|
85
|
+
raise ArgumentError.new('nil or empty event') if event.empty?
|
86
|
+
args = args.reduce('') do |memo, arg|
|
87
|
+
memo << "#{arg}\r\n"
|
88
|
+
end
|
89
|
+
return "#{event}\r\n#{args}\r\n"
|
90
|
+
end
|
91
|
+
|
92
|
+
def format_message(*args)
|
93
|
+
self.class.format_message(*args)
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.parse_message(message)
|
97
|
+
message = message.lines.map(&:chomp) if message.is_a?(String)
|
98
|
+
return [nil, []] if message.nil?
|
99
|
+
event = message.first.match(/^:?(\w+)/)
|
100
|
+
event = event[1].to_s.downcase.to_sym unless event.nil?
|
101
|
+
args = message.slice(1, message.length) || []
|
102
|
+
args.pop if args.last.nil? || args.last.empty?
|
103
|
+
return [event, args]
|
104
|
+
end
|
105
|
+
|
106
|
+
def parse_message(*args)
|
107
|
+
self.class.parse_message(*args)
|
108
|
+
end
|
109
|
+
|
110
|
+
def self.get_message(socket)
|
111
|
+
message = []
|
112
|
+
while line = socket.gets
|
113
|
+
if line.nil? || (line = line.strip).empty?
|
114
|
+
break
|
115
|
+
else
|
116
|
+
message << line
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
if message.empty?
|
121
|
+
return nil
|
122
|
+
else
|
123
|
+
return parse_message(message)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
def get_message(*args) self.class.get_message(*args); end
|
127
|
+
end
|
128
|
+
|
129
|
+
TcpSyncDemultiplexer = TcpSyncDemux
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'functional'
|
3
|
+
|
4
|
+
behavior_info(:runnable,
|
5
|
+
run: 0,
|
6
|
+
stop: 0,
|
7
|
+
running?: 0)
|
8
|
+
|
9
|
+
module Concurrent
|
10
|
+
|
11
|
+
class Supervisor
|
12
|
+
|
13
|
+
DEFAULT_MONITOR_INTERVAL = 1
|
14
|
+
|
15
|
+
behavior(:runnable)
|
16
|
+
|
17
|
+
WorkerContext = Struct.new(:worker, :thread)
|
18
|
+
|
19
|
+
attr_reader :monitor_interval
|
20
|
+
|
21
|
+
def initialize(opts = {})
|
22
|
+
@mutex = Mutex.new
|
23
|
+
@workers = []
|
24
|
+
@running = false
|
25
|
+
@monitor = nil
|
26
|
+
@monitor_interval = opts[:monitor] || opts[:monitor_interval] || DEFAULT_MONITOR_INTERVAL
|
27
|
+
add_worker(opts[:worker]) unless opts[:worker].nil?
|
28
|
+
end
|
29
|
+
|
30
|
+
def run!
|
31
|
+
raise StandardError.new('already running') if running?
|
32
|
+
@running = true
|
33
|
+
@monitor = Thread.new{ monitor }
|
34
|
+
Thread.pass
|
35
|
+
end
|
36
|
+
|
37
|
+
def run
|
38
|
+
raise StandardError.new('already running') if running?
|
39
|
+
@running = true
|
40
|
+
monitor
|
41
|
+
end
|
42
|
+
|
43
|
+
def stop
|
44
|
+
return true unless running?
|
45
|
+
@running = false
|
46
|
+
@mutex.synchronize do
|
47
|
+
Thread.kill(@monitor) unless @monitor.nil?
|
48
|
+
@monitor = nil
|
49
|
+
|
50
|
+
until @workers.empty?
|
51
|
+
context = @workers.pop
|
52
|
+
begin
|
53
|
+
context.worker.stop
|
54
|
+
Thread.pass
|
55
|
+
rescue Exception => ex
|
56
|
+
# suppress
|
57
|
+
ensure
|
58
|
+
Thread.kill(context.thread) unless context.thread.nil?
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def running?
|
65
|
+
return @running
|
66
|
+
end
|
67
|
+
|
68
|
+
def length
|
69
|
+
return @workers.length
|
70
|
+
end
|
71
|
+
alias_method :size, :length
|
72
|
+
|
73
|
+
def add_worker(worker)
|
74
|
+
if worker.nil? || running? || ! worker.behaves_as?(:runnable)
|
75
|
+
return false
|
76
|
+
else
|
77
|
+
@mutex.synchronize {
|
78
|
+
@workers << WorkerContext.new(worker)
|
79
|
+
}
|
80
|
+
return true
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def monitor
|
87
|
+
loop do
|
88
|
+
@mutex.synchronize do
|
89
|
+
@workers.each do |context|
|
90
|
+
unless context.thread && context.thread.alive?
|
91
|
+
context.thread = Thread.new{ context.worker.run }
|
92
|
+
context.thread.abort_on_exception = false
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
sleep(@monitor_interval)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|