zmachine 0.1.3 → 0.2.0
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.
- checksums.yaml +6 -14
- data/.gitignore +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +5 -2
- data/README.md +3 -1
- data/Rakefile +1 -0
- data/benchmarks/benchmark.sh +12 -0
- data/benchmarks/tcp_channel.rb +26 -0
- data/benchmarks/zmq_channel.rb +23 -0
- data/echo_client.rb +10 -28
- data/echo_server.rb +15 -22
- data/lib/zmachine.rb +71 -23
- data/lib/zmachine/channel.rb +29 -27
- data/lib/zmachine/connection.rb +178 -51
- data/lib/zmachine/connection_manager.rb +133 -0
- data/lib/zmachine/hashed_wheel.rb +23 -31
- data/lib/zmachine/reactor.rb +66 -393
- data/lib/zmachine/tcp_channel.rb +47 -109
- data/lib/zmachine/zmq_channel.rb +74 -115
- data/spec/connection_manager_spec.rb +16 -0
- data/spec/connection_spec.rb +56 -0
- data/spec/hashed_wheel_spec.rb +15 -12
- data/spec/spec_helper.rb +22 -14
- data/spec/tcp_channel_spec.rb +109 -0
- data/spec/zmq_channel_spec.rb +113 -0
- data/zmachine.gemspec +4 -2
- metadata +54 -26
data/lib/zmachine/connection.rb
CHANGED
@@ -1,87 +1,102 @@
|
|
1
|
+
java_import java.io.IOException
|
2
|
+
java_import java.nio.ByteBuffer
|
3
|
+
java_import java.nio.channels.SelectionKey
|
4
|
+
|
1
5
|
module ZMachine
|
2
6
|
class Connection
|
3
7
|
|
4
|
-
attr_accessor :channel
|
5
|
-
|
6
8
|
extend Forwardable
|
7
9
|
|
8
|
-
|
10
|
+
attr_accessor :channel
|
9
11
|
|
10
|
-
def self.new(
|
12
|
+
def self.new(*args, &block)
|
11
13
|
allocate.instance_eval do
|
12
|
-
@channel = channel
|
13
14
|
initialize(*args, &block)
|
14
15
|
post_init
|
15
16
|
self
|
16
17
|
end
|
17
18
|
end
|
18
19
|
|
19
|
-
|
20
|
-
end
|
21
|
-
|
22
|
-
# callbacks
|
20
|
+
# channel type dispatch
|
23
21
|
|
24
|
-
def
|
22
|
+
def bind(address, port_or_type)
|
23
|
+
ZMachine.logger.debug("zmachine:connection:#{__method__}", connection: self) if ZMachine.debug
|
24
|
+
if address =~ %r{\w+://}
|
25
|
+
@channel = ZMQChannel.new(port_or_type)
|
26
|
+
@channel.bind(address)
|
27
|
+
else
|
28
|
+
@channel = TCPChannel.new
|
29
|
+
@channel.bind(address, port_or_type)
|
30
|
+
end
|
31
|
+
self
|
25
32
|
end
|
26
33
|
|
27
|
-
def
|
34
|
+
def connect(address, port_or_type)
|
35
|
+
ZMachine.logger.debug("zmachine:connection:#{__method__}", connection: self) if ZMachine.debug
|
36
|
+
if address.nil? or address =~ %r{\w+://}
|
37
|
+
@channel = ZMQChannel.new(port_or_type)
|
38
|
+
@channel.connect(address) if address
|
39
|
+
else
|
40
|
+
@channel = TCPChannel.new
|
41
|
+
@channel.connect(address, port_or_type)
|
42
|
+
end
|
43
|
+
if @connect_timeout
|
44
|
+
@timer = ZMachine.add_timer(@connect_timeout) do
|
45
|
+
ZMachine.reactor.close_connection(self)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
self
|
28
49
|
end
|
29
50
|
|
30
|
-
|
31
|
-
|
51
|
+
def_delegator :@channel, :bound?
|
52
|
+
def_delegator :@channel, :closed?
|
53
|
+
def_delegator :@channel, :connected?
|
54
|
+
def_delegator :@channel, :connection_pending?
|
32
55
|
|
33
|
-
|
34
|
-
@channel.send3 a,b,c
|
35
|
-
end
|
36
|
-
def send2(a,b)
|
37
|
-
@channel.send2 a,b
|
38
|
-
end
|
56
|
+
# EventMachine Connection API
|
39
57
|
|
40
|
-
def
|
58
|
+
def _not_implemented
|
59
|
+
raise RuntimeError.new("API call not implemented! #{caller[0]}")
|
41
60
|
end
|
42
61
|
|
43
|
-
def
|
44
|
-
|
62
|
+
def close_connection(after_writing = false)
|
63
|
+
@channel.close(after_writing)
|
45
64
|
end
|
46
65
|
|
47
|
-
|
48
|
-
|
49
|
-
def_delegator :@channel, :close_connection
|
66
|
+
alias :close :close_connection
|
50
67
|
|
51
68
|
def close_connection_after_writing
|
52
69
|
close_connection(true)
|
53
70
|
end
|
54
71
|
|
72
|
+
alias :close_after_writing close_connection_after_writing
|
73
|
+
|
55
74
|
def comm_inactivity_timeout
|
56
|
-
|
75
|
+
@inactivity_timeout
|
57
76
|
end
|
58
77
|
|
59
78
|
def comm_inactivity_timeout=(value)
|
60
|
-
|
79
|
+
@inactivity_timeout = value
|
61
80
|
end
|
62
81
|
|
63
82
|
alias :set_comm_inactivity_timeout :comm_inactivity_timeout=
|
64
83
|
|
84
|
+
def connection_accepted(channel)
|
85
|
+
end
|
86
|
+
|
87
|
+
def connection_completed
|
88
|
+
end
|
89
|
+
|
65
90
|
def detach
|
66
91
|
_not_implemented
|
67
92
|
end
|
68
93
|
|
69
94
|
def error?
|
70
|
-
|
71
|
-
case errno
|
72
|
-
when 0
|
73
|
-
false
|
74
|
-
when -1
|
75
|
-
true
|
76
|
-
else
|
77
|
-
Errno::constants.select do |name|
|
78
|
-
Errno.__send__(:const_get, name)::Errno == errno
|
79
|
-
end.first
|
80
|
-
end
|
95
|
+
_not_implemented
|
81
96
|
end
|
82
97
|
|
83
98
|
def get_idle_time
|
84
|
-
|
99
|
+
(System.nano_time - @last_activity) / 1_000_000
|
85
100
|
end
|
86
101
|
|
87
102
|
def get_peer_cert
|
@@ -89,7 +104,7 @@ module ZMachine
|
|
89
104
|
end
|
90
105
|
|
91
106
|
def get_peername
|
92
|
-
if peer = @channel.
|
107
|
+
if peer = @channel.peer
|
93
108
|
::Socket.pack_sockaddr_in(*peer)
|
94
109
|
end
|
95
110
|
end
|
@@ -102,15 +117,16 @@ module ZMachine
|
|
102
117
|
_not_implemented
|
103
118
|
end
|
104
119
|
|
105
|
-
|
120
|
+
def get_sock_opt(level, option)
|
121
|
+
_not_implemented
|
122
|
+
end
|
106
123
|
|
107
124
|
def get_sockname
|
108
|
-
|
109
|
-
::Socket.pack_sockaddr_in(*sock_name)
|
110
|
-
end
|
125
|
+
_not_implemented
|
111
126
|
end
|
112
127
|
|
113
128
|
def get_status
|
129
|
+
_not_implemented
|
114
130
|
end
|
115
131
|
|
116
132
|
def notify_readable=(mode)
|
@@ -118,7 +134,7 @@ module ZMachine
|
|
118
134
|
end
|
119
135
|
|
120
136
|
def notify_readable?
|
121
|
-
|
137
|
+
true
|
122
138
|
end
|
123
139
|
|
124
140
|
def notify_writable=(mode)
|
@@ -126,7 +142,7 @@ module ZMachine
|
|
126
142
|
end
|
127
143
|
|
128
144
|
def notify_writable?
|
129
|
-
|
145
|
+
@channel.can_send?
|
130
146
|
end
|
131
147
|
|
132
148
|
def pause
|
@@ -138,10 +154,14 @@ module ZMachine
|
|
138
154
|
end
|
139
155
|
|
140
156
|
def pending_connect_timeout=(value)
|
157
|
+
@connect_timeout = value
|
141
158
|
end
|
142
159
|
|
143
160
|
alias :set_pending_connect_timeout :pending_connect_timeout=
|
144
161
|
|
162
|
+
def post_init
|
163
|
+
end
|
164
|
+
|
145
165
|
def proxy_completed
|
146
166
|
_not_implemented
|
147
167
|
end
|
@@ -154,11 +174,22 @@ module ZMachine
|
|
154
174
|
_not_implemented
|
155
175
|
end
|
156
176
|
|
177
|
+
def receive_data(data)
|
178
|
+
end
|
179
|
+
|
180
|
+
def reconnect(server, port_or_type)
|
181
|
+
ZMachine.reconnect(server, port_or_type, self)
|
182
|
+
end
|
183
|
+
|
157
184
|
def resume
|
158
185
|
_not_implemented
|
159
186
|
end
|
160
187
|
|
161
|
-
|
188
|
+
def send_data(data)
|
189
|
+
ZMachine.logger.debug("zmachine:connection:#{__method__}", connection: self) if ZMachine.debug
|
190
|
+
@channel.send_data(data)
|
191
|
+
update_events
|
192
|
+
end
|
162
193
|
|
163
194
|
def send_datagram(data, recipient_address, recipient_port)
|
164
195
|
_not_implemented
|
@@ -168,7 +199,9 @@ module ZMachine
|
|
168
199
|
_not_implemented
|
169
200
|
end
|
170
201
|
|
171
|
-
|
202
|
+
def set_sock_opt(level, optname, optval)
|
203
|
+
_not_implemented
|
204
|
+
end
|
172
205
|
|
173
206
|
def ssl_handshake_completed
|
174
207
|
_not_implemented
|
@@ -190,10 +223,104 @@ module ZMachine
|
|
190
223
|
_not_implemented
|
191
224
|
end
|
192
225
|
|
193
|
-
|
226
|
+
def unbind
|
227
|
+
end
|
194
228
|
|
195
|
-
|
196
|
-
|
229
|
+
# triggers
|
230
|
+
|
231
|
+
def acceptable!
|
232
|
+
client = @channel.accept
|
233
|
+
connection_accepted(client) if client.connected?
|
234
|
+
ZMachine.logger.debug("zmachine:connection:#{__method__}", connection: self, client: client) if ZMachine.debug
|
235
|
+
self.class.new.tap do |connection|
|
236
|
+
connection.channel = client
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
def connectable!
|
241
|
+
ZMachine.logger.debug("zmachine:connection:#{__method__}", connection: self) if ZMachine.debug
|
242
|
+
@channel.finish_connecting
|
243
|
+
@timer.cancel if @timer # cancel pending connect timer
|
244
|
+
mark_active!
|
245
|
+
connection_completed if @channel.connected?
|
246
|
+
update_events
|
247
|
+
nil
|
197
248
|
end
|
249
|
+
|
250
|
+
def readable!
|
251
|
+
ZMachine.logger.debug("zmachine:connection:#{__method__}", connection: self) if ZMachine.debug
|
252
|
+
mark_active!
|
253
|
+
data = @channel.read_inbound_data
|
254
|
+
receive_data(data) if data
|
255
|
+
nil
|
256
|
+
end
|
257
|
+
|
258
|
+
def writable!
|
259
|
+
ZMachine.logger.debug("zmachine:connection:#{__method__}", connection: self) if ZMachine.debug
|
260
|
+
mark_active!
|
261
|
+
@channel.write_outbound_data
|
262
|
+
update_events
|
263
|
+
nil
|
264
|
+
end
|
265
|
+
|
266
|
+
# selector registration
|
267
|
+
|
268
|
+
def register(selector)
|
269
|
+
ZMachine.logger.debug("zmachine:connection:#{__method__}", connection: self, fd: @channel.selectable_fd) if ZMachine.debug
|
270
|
+
@channel_key ||= @channel.selectable_fd.register(selector, current_events, self)
|
271
|
+
end
|
272
|
+
|
273
|
+
def update_events
|
274
|
+
return unless @channel_key
|
275
|
+
ZMachine.logger.debug("zmachine:connection:#{__method__}", connection: self) if ZMachine.debug
|
276
|
+
@channel_key.interest_ops(current_events)
|
277
|
+
end
|
278
|
+
|
279
|
+
def current_events
|
280
|
+
if @channel.is_a?(ZMQChannel)
|
281
|
+
return SelectionKey::OP_READ
|
282
|
+
end
|
283
|
+
|
284
|
+
if bound?
|
285
|
+
return SelectionKey::OP_ACCEPT
|
286
|
+
end
|
287
|
+
|
288
|
+
if connection_pending?
|
289
|
+
return SelectionKey::OP_CONNECT
|
290
|
+
end
|
291
|
+
|
292
|
+
events = 0
|
293
|
+
|
294
|
+
events |= SelectionKey::OP_READ if notify_readable?
|
295
|
+
events |= SelectionKey::OP_WRITE if notify_writable?
|
296
|
+
|
297
|
+
return events
|
298
|
+
end
|
299
|
+
|
300
|
+
def process_events
|
301
|
+
return unless @channel_key
|
302
|
+
ZMachine.logger.debug("zmachine:connection:#{__method__}", connection: self) if ZMachine.debug
|
303
|
+
if @channel_key.connectable?
|
304
|
+
connectable!
|
305
|
+
elsif @channel_key.acceptable?
|
306
|
+
acceptable!
|
307
|
+
else
|
308
|
+
writable! if @channel_key.writable?
|
309
|
+
readable! if @channel_key.readable?
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
def mark_active!
|
314
|
+
@last_activity = System.nano_time
|
315
|
+
renew_timer if @inactivity_timeout
|
316
|
+
end
|
317
|
+
|
318
|
+
def renew_timer
|
319
|
+
@timer.cancel if @timer
|
320
|
+
@timer = ZMachine.add_timer(@inactivity_timeout) do
|
321
|
+
ZMachine.reactor.close_connection(self)
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
198
325
|
end
|
199
326
|
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
java_import java.nio.channels.ClosedChannelException
|
2
|
+
|
3
|
+
require 'zmachine/tcp_channel'
|
4
|
+
require 'zmachine/zmq_channel'
|
5
|
+
|
6
|
+
module ZMachine
|
7
|
+
class ConnectionManager
|
8
|
+
|
9
|
+
attr_reader :connections
|
10
|
+
|
11
|
+
def initialize(selector)
|
12
|
+
ZMachine.logger.debug("zmachine:connection_manager:#{__method__}") if ZMachine.debug
|
13
|
+
@selector = selector
|
14
|
+
@connections = []
|
15
|
+
@zmq_connections = []
|
16
|
+
@new_connections = []
|
17
|
+
@unbound_connections = []
|
18
|
+
end
|
19
|
+
|
20
|
+
def idle?
|
21
|
+
@new_connections.size == 0 and
|
22
|
+
@zmq_connections.none? {|c| c.channel.has_more? } # see comment in #process
|
23
|
+
end
|
24
|
+
|
25
|
+
def shutdown
|
26
|
+
ZMachine.logger.debug("zmachine:connection_manager:#{__method__}") if ZMachine.debug
|
27
|
+
@unbound_connections += @connections
|
28
|
+
cleanup
|
29
|
+
end
|
30
|
+
|
31
|
+
def bind(address, port_or_type, handler, *args, &block)
|
32
|
+
ZMachine.logger.debug("zmachine:connection_manager:#{__method__}", address: address, port_or_type: port_or_type) if ZMachine.debug
|
33
|
+
connection = build_connection(Connection, handler, *args, &block)
|
34
|
+
connection.bind(address, port_or_type)
|
35
|
+
@new_connections << connection
|
36
|
+
end
|
37
|
+
|
38
|
+
def connect(address, port_or_type, handler, *args, &block)
|
39
|
+
ZMachine.logger.debug("zmachine:connection_manager:#{__method__}", address: address, port_or_type: port_or_type) if ZMachine.debug
|
40
|
+
connection = build_connection(Connection, handler, *args, &block)
|
41
|
+
connection.connect(address, port_or_type)
|
42
|
+
@new_connections << connection
|
43
|
+
yield connection if block_given?
|
44
|
+
rescue java.nio.channels.UnresolvedAddressException
|
45
|
+
raise ZMachine::ConnectionError.new('unable to resolve server address')
|
46
|
+
end
|
47
|
+
|
48
|
+
def process
|
49
|
+
ZMachine.logger.debug("zmachine:connection_manager:#{__method__}") if ZMachine.debug
|
50
|
+
add_new_connections
|
51
|
+
it = @selector.selected_keys.iterator
|
52
|
+
while it.has_next
|
53
|
+
process_connection(it.next.attachment)
|
54
|
+
it.remove
|
55
|
+
end
|
56
|
+
# super ugly, but ZMQ only triggers the FD if and only if you have read
|
57
|
+
# every message from the socket. under load however there will always be
|
58
|
+
# new messages in the mailbox between last recv and next select, which
|
59
|
+
# causes the FD never to be triggered again.
|
60
|
+
# the only mitigation strategy i came up with is iterating over all channels :(
|
61
|
+
@zmq_connections.each do |connection|
|
62
|
+
connection.readable! if connection.channel.has_more?
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def process_connection(connection)
|
67
|
+
new_connection = connection.process_events
|
68
|
+
@new_connections << new_connection if new_connection
|
69
|
+
rescue IOException
|
70
|
+
close_connection(connection)
|
71
|
+
end
|
72
|
+
|
73
|
+
def close_connection(connection)
|
74
|
+
ZMachine.logger.debug("zmachine:connection_manager:#{__method__}", connection: connection) if ZMachine.debug
|
75
|
+
@unbound_connections << connection
|
76
|
+
end
|
77
|
+
|
78
|
+
def add_new_connections
|
79
|
+
@new_connections.compact.each do |connection|
|
80
|
+
ZMachine.logger.debug("zmachine:connection_manager:#{__method__}", connection: connection) if ZMachine.debug
|
81
|
+
begin
|
82
|
+
connection.register(@selector)
|
83
|
+
@connections << connection
|
84
|
+
@zmq_connections << connection if connection.channel.is_a?(ZMQChannel)
|
85
|
+
rescue ClosedChannelException => e
|
86
|
+
ZMachine.logger.exception(e, "failed to add connection")
|
87
|
+
@unbound_connections << connection
|
88
|
+
end
|
89
|
+
end
|
90
|
+
@new_connections.clear
|
91
|
+
end
|
92
|
+
|
93
|
+
def cleanup
|
94
|
+
return if @unbound_connections.empty?
|
95
|
+
ZMachine.logger.debug("zmachine:connection_manager:#{__method__}") if ZMachine.debug
|
96
|
+
@unbound_connections.each do |connection|
|
97
|
+
reason = nil
|
98
|
+
connection, reason = *connection if connection.is_a?(Array)
|
99
|
+
begin
|
100
|
+
@connections.delete(connection)
|
101
|
+
@zmq_connections.delete(connection)
|
102
|
+
connection.unbind
|
103
|
+
connection.close
|
104
|
+
rescue Exception => e
|
105
|
+
ZMachine.logger.exception(e, "failed to unbind connection") if ZMachine.debug
|
106
|
+
end
|
107
|
+
end
|
108
|
+
@unbound_connections.clear
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
def build_connection(klass = Connection, handler = nil, *args, &block)
|
114
|
+
if handler and handler.is_a?(Class)
|
115
|
+
handler.new(*args, &block)
|
116
|
+
elsif handler and handler.is_a?(Connection)
|
117
|
+
# already initialized connection on reconnect
|
118
|
+
handler
|
119
|
+
elsif handler
|
120
|
+
connection_from_module(klass, handler).new(*args, &block)
|
121
|
+
else
|
122
|
+
klass.new(*args, &block)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def connection_from_module(klass, handler)
|
127
|
+
handler::CONNECTION_CLASS
|
128
|
+
rescue NameError
|
129
|
+
handler::const_set(:CONNECTION_CLASS, Class.new(klass) { include handler })
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
133
|
+
end
|