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.
@@ -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
- alias original_method method
10
+ attr_accessor :channel
9
11
 
10
- def self.new(channel, *args, &block)
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
- def initialize(*args, &block)
20
- end
21
-
22
- # callbacks
20
+ # channel type dispatch
23
21
 
24
- def post_init
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 connection_completed
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
- def receive_data(data)
31
- end
51
+ def_delegator :@channel, :bound?
52
+ def_delegator :@channel, :closed?
53
+ def_delegator :@channel, :connected?
54
+ def_delegator :@channel, :connection_pending?
32
55
 
33
- def send3(a,b,c)
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 unbind
58
+ def _not_implemented
59
+ raise RuntimeError.new("API call not implemented! #{caller[0]}")
41
60
  end
42
61
 
43
- def reconnect(server, port)
44
- ZMachine.reconnect server, port, self
62
+ def close_connection(after_writing = false)
63
+ @channel.close(after_writing)
45
64
  end
46
65
 
47
- # public API
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
- channel.comm_inactivity_timeout
75
+ @inactivity_timeout
57
76
  end
58
77
 
59
78
  def comm_inactivity_timeout=(value)
60
- channel.comm_inactivity_timeout = value
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
- errno = ZMachine::report_connection_error_status(@signature)
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
- _not_implemented
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.peer_name
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
- def_delegator :@channel, :get_sock_opt
120
+ def get_sock_opt(level, option)
121
+ _not_implemented
122
+ end
106
123
 
107
124
  def get_sockname
108
- if sock_name = @channel.sock_name
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
- _not_implemented
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
- _not_implemented
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
- def_delegator :@channel, :send_data
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
- def_delegator :@channel, :set_sock_opt
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
- private
226
+ def unbind
227
+ end
194
228
 
195
- def _not_implemented
196
- raise RuntimeError.new("API call not implemented! #{caller[0]}")
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