zmachine 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,337 @@
1
+ java_import java.io.FileDescriptor
2
+ java_import java.io.IOException
3
+ java_import java.net.InetSocketAddress
4
+ java_import java.nio.ByteBuffer
5
+ java_import java.nio.channels.ClosedChannelException
6
+ java_import java.nio.channels.SelectionKey
7
+ java_import java.nio.channels.Selector
8
+ java_import java.nio.channels.ServerSocketChannel
9
+ java_import java.nio.channels.SocketChannel
10
+ java_import java.util.TreeMap
11
+ java_import java.util.concurrent.atomic.AtomicBoolean
12
+ java_import java.util.concurrent.ConcurrentLinkedQueue
13
+
14
+ require 'zmachine/jeromq-0.3.0-SNAPSHOT.jar'
15
+ java_import org.zeromq.ZContext
16
+
17
+ require 'zmachine/acceptor'
18
+ require 'zmachine/tcp_channel'
19
+ require 'zmachine/zmq_channel'
20
+
21
+ module ZMachine
22
+ class Reactor
23
+
24
+ def initialize
25
+ @timers = TreeMap.new
26
+ @timer_callbacks = {}
27
+ @channels = []
28
+ @new_channels = []
29
+ @unbound_channels = []
30
+ @next_signature = 0
31
+ @shutdown_hooks = []
32
+ @next_tick_queue = ConcurrentLinkedQueue.new
33
+ @running = false
34
+
35
+ # don't use a direct buffer. Ruby doesn't seem to like them.
36
+ @read_buffer = ByteBuffer.allocate(32*1024)
37
+ end
38
+
39
+ def add_shutdown_hook(&block)
40
+ @shutdown_hooks << block
41
+ end
42
+
43
+ def add_timer(*args, &block)
44
+ interval = args.shift
45
+ callback = args.shift || block
46
+ return unless callback
47
+
48
+ signature = next_signature
49
+ deadline = java.util.Date.new.time + (interval.to_f * 1000).to_i
50
+
51
+ if @timers.contains_key(deadline)
52
+ @timers.get(deadline) << signature
53
+ else
54
+ @timers.put(deadline, [signature])
55
+ end
56
+
57
+ @timer_callbacks[signature] = callback
58
+ signature
59
+ end
60
+
61
+ def cancel_timer(timer_or_sig)
62
+ if timer_or_sig.respond_to?(:cancel)
63
+ timer_or_sig.cancel
64
+ else
65
+ @timer_callbacks[timer_or_sig] = false if @timer_callbacks.has_key?(timer_or_sig)
66
+ end
67
+ end
68
+
69
+ def connect(server, port_or_type=nil, handler=nil, *args, &block)
70
+ if server.nil? or server =~ %r{\w+://}
71
+ _connect_zmq(server, port_or_type, handler, *args, &block)
72
+ else
73
+ _connect_tcp(server, port_or_type, handler, *args, &block)
74
+ end
75
+ end
76
+
77
+ def connection_count
78
+ @channels.size
79
+ end
80
+
81
+ def error_handler(callback = nil, &block)
82
+ @error_handler = callback || block
83
+ end
84
+
85
+ def next_tick(callback=nil, &block)
86
+ @next_tick_queue << (callback || block)
87
+ signal_loopbreak if running?
88
+ end
89
+
90
+ def run(callback=nil, shutdown_hook=nil, &block)
91
+ @callback = callback || block
92
+
93
+ add_shutdown_hook(shutdown_hook) if shutdown_hook
94
+
95
+ begin
96
+ @running = true
97
+
98
+ add_timer(0, @callback) if @callback
99
+
100
+ @selector = Selector.open
101
+ @run_reactor = true
102
+
103
+ while @run_reactor
104
+ run_deferred_callbacks
105
+ break unless @run_reactor
106
+ run_timers
107
+ break unless @run_reactor
108
+ remove_unbound_channels
109
+ check_io
110
+ add_new_channels
111
+ process_io
112
+ end
113
+ ensure
114
+ @selector.close rescue nil
115
+ @selector = nil
116
+ @unbound_channels += @channels
117
+ remove_unbound_channels
118
+ @shutdown_hooks.pop.call until @shutdown_hooks.empty?
119
+ @next_tick_queue = ConcurrentLinkedQueue.new
120
+ @running = false
121
+ ZMachine.context.destroy
122
+ end
123
+ end
124
+
125
+ def reactor_running?
126
+ @running || false
127
+ end
128
+
129
+ def start_server(server, port_or_type=nil, handler=nil, *args, &block)
130
+ if server =~ %r{\w+://}
131
+ _bind_zmq(server, port_or_type, handler, *args, &block)
132
+ else
133
+ _bind_tcp(server, port_or_type, handler, *args, &block)
134
+ end
135
+ end
136
+
137
+ def stop_event_loop
138
+ @run_reactor = false
139
+ signal_loopbreak
140
+ end
141
+
142
+ def stop_server(channel)
143
+ channel.close
144
+ end
145
+
146
+ private
147
+
148
+ def _bind_tcp(address, port, handler, *args, &block)
149
+ klass = _klass_from_handler(Connection, handler)
150
+ channel = TCPChannel.new(@selector)
151
+ channel.bind(address, port)
152
+ add_channel(channel, Acceptor, klass, *args, &block)
153
+ end
154
+
155
+ def _bind_zmq(address, type, handler, *args, &block)
156
+ klass = _klass_from_handler(Connection, handler)
157
+ channel = ZMQChannel.new(type, @selector)
158
+ channel.bind(address)
159
+ add_channel(channel, klass, *args, &block)
160
+ end
161
+
162
+ def _connect_tcp(address, port, handler=nil, *args, &block)
163
+ klass = _klass_from_handler(Connection, handler)
164
+ channel = TCPChannel.new(@selector)
165
+ channel.connect(address, port)
166
+ channel.connect_pending = true
167
+ add_channel(channel, klass, *args, &block)
168
+ end
169
+
170
+ def _connect_zmq(address, type, handler=nil, *args, &block)
171
+ klass = _klass_from_handler(Connection, handler)
172
+ channel = ZMQChannel.new(type, @selector)
173
+ add_channel(channel, klass, *args, &block)
174
+ channel.connect(address)
175
+ channel.handler.connection_completed
176
+ channel
177
+ end
178
+
179
+ def check_io
180
+ if @new_channels.size > 0
181
+ timeout = -1
182
+ elsif !@timers.empty?
183
+ now = java.util.Date.new.time
184
+ timer_key = @timers.first_key
185
+ timeout = timer_key - now
186
+ timeout = -1 if timeout <= 0
187
+ else
188
+ timeout = 0
189
+ end
190
+
191
+ if @channels.any?(&:has_more?)
192
+ timeout = -1
193
+ end
194
+
195
+ if timeout == -1
196
+ @selector.select_now
197
+ else
198
+ @selector.select(timeout)
199
+ end
200
+ end
201
+
202
+ def process_io
203
+ it = @selector.selected_keys.iterator
204
+
205
+ while it.has_next
206
+ selected_key = it.next
207
+ it.remove
208
+
209
+ if selected_key.connectable?
210
+ is_connectable(selected_key.attachment)
211
+ elsif selected_key.acceptable?
212
+ is_acceptable(selected_key.attachment)
213
+ else
214
+ is_writable(selected_key.attachment) if selected_key.writable?
215
+ is_readable(selected_key.attachment) if selected_key.readable?
216
+ end
217
+ end
218
+
219
+ @channels.each do |channel|
220
+ is_readable(channel) if channel.has_more?
221
+ is_writable(channel) if channel.can_send?
222
+ end
223
+ end
224
+
225
+ def is_acceptable(channel)
226
+ client_channel = channel.accept(next_signature)
227
+ acceptor = channel.handler
228
+ add_channel(client_channel, acceptor.klass, *acceptor.args, &acceptor.callback)
229
+ rescue IOException
230
+ channel.close
231
+ end
232
+
233
+ def is_readable(channel)
234
+ data = channel.read_inbound_data(@read_buffer)
235
+ channel.handler.receive_data(data) if data
236
+ rescue IOException
237
+ @unbound_channels << channel
238
+ end
239
+
240
+ def is_writable(channel)
241
+ @unbound_channels << channel unless channel.write_outbound_data
242
+ rescue IOException
243
+ @unbound_channels << channel
244
+ end
245
+
246
+ def is_connectable(channel)
247
+ channel.finish_connecting
248
+ channel.handler.connection_completed
249
+ rescue IOException
250
+ @unbound_channels << channel
251
+ end
252
+
253
+ def add_channel(channel, klass, *args, &block)
254
+ channel.handler = klass.new(channel, *args)
255
+ @new_channels << channel
256
+ block.call(channel.handler) if block
257
+ channel
258
+ end
259
+
260
+ def add_new_channels
261
+ @new_channels.each do |channel|
262
+ begin
263
+ channel.register
264
+ @channels << channel
265
+ rescue ClosedChannelException
266
+ @unbound_channels << channel
267
+ end
268
+ end
269
+ @new_channels.clear
270
+ end
271
+
272
+ def remove_unbound_channels
273
+ @unbound_channels.each do |channel|
274
+ channel.handler.unbind if channel.handler
275
+ channel.close
276
+ end
277
+ @unbound_channels.clear
278
+ end
279
+
280
+ def run_deferred_callbacks
281
+ # max is current size
282
+ size = @next_tick_queue.size
283
+ while size > 0 && callback = @next_tick_queue.poll
284
+ begin
285
+ size -= 1
286
+ callback.call
287
+ ensure
288
+ ZMachine.next_tick {} if $!
289
+ end
290
+ end
291
+ end
292
+
293
+ def run_timers
294
+ now = java.util.Date.new.time
295
+ until @timers.empty?
296
+ timer_key = @timers.first_key
297
+ break if timer_key > now
298
+
299
+ signatures = @timers.get(timer_key)
300
+ @timers.remove(timer_key)
301
+
302
+ # Fire all timers at this timestamp
303
+ signatures.each do |signature|
304
+ callback = @timer_callbacks.delete(signature)
305
+ return if callback == false # callback cancelled
306
+ callback or raise UnknownTimerFired, "callback data: #{signature}"
307
+ callback.call
308
+ end
309
+ end
310
+ end
311
+
312
+ def signal_loopbreak
313
+ @selector.wakeup if @selector
314
+ end
315
+
316
+ def next_signature
317
+ @next_signature += 1
318
+ end
319
+
320
+ def _klass_from_handler(klass = Connection, handler = nil)
321
+ if handler and handler.is_a?(Class)
322
+ handler
323
+ elsif handler
324
+ _handler_from_klass(klass, handler)
325
+ else
326
+ klass
327
+ end
328
+ end
329
+
330
+ def _handler_from_module(klass, handler)
331
+ handler::CONNECTION_CLASS
332
+ rescue NameError
333
+ handler::const_set(:CONNECTION_CLASS, Class.new(klass) { include handler })
334
+ end
335
+
336
+ end
337
+ end
@@ -0,0 +1,169 @@
1
+ java_import java.nio.channels.ServerSocketChannel
2
+
3
+ require 'zmachine/channel'
4
+
5
+ module ZMachine
6
+ class TCPChannel < Channel
7
+
8
+ attr_reader :connect_pending
9
+
10
+ def initialize(selector)
11
+ super(selector)
12
+ @close_scheduled = false
13
+ @connect_pending = false
14
+ end
15
+
16
+ def register
17
+ @channel_key ||= @socket.register(@selector, current_events, self)
18
+ end
19
+
20
+ def bind(address, port)
21
+ address = InetSocketAddress.new(address, port)
22
+ @socket = ServerSocketChannel.open
23
+ @socket.configure_blocking(false)
24
+ @socket.bind(address)
25
+ end
26
+
27
+ def accept
28
+ client_socket = socket.accept
29
+ return unless client_socket
30
+ client_socket.configure_blocking(false)
31
+ TCPChannel.new(client_socket, @selector)
32
+ end
33
+
34
+ def connect(address, port)
35
+ address = InetSocketAddress.new(address, port)
36
+ @socket = SocketChannel.open
37
+ @socket.configure_blocking(false)
38
+
39
+ if socket.connect(address)
40
+ # Connection returned immediately. Can happen with localhost
41
+ # connections.
42
+ # WARNING, this code is untested due to lack of available test
43
+ # conditions. Ought to be be able to come here from a localhost
44
+ # connection, but that doesn't happen on Linux. (Maybe on FreeBSD?)
45
+ # The reason for not handling this until we can test it is that we
46
+ # really need to return from this function WITHOUT triggering any EM
47
+ # events. That's because until the user code has seen the signature
48
+ # we generated here, it won't be able to properly dispatch them. The
49
+ # C++ EM deals with this by setting pending mode as a flag in ALL
50
+ # eventable descriptors and making the descriptor select for
51
+ # writable. Then, it can send UNBOUND and CONNECTION_COMPLETED on the
52
+ # next pass through the loop, because writable will fire.
53
+ raise RuntimeError.new("immediate-connect unimplemented")
54
+ end
55
+ end
56
+
57
+ def close
58
+ if @channel_key
59
+ @channel_key.cancel
60
+ @channel_key = nil
61
+ end
62
+
63
+ @socket.close rescue nil
64
+ end
65
+
66
+ def send_data(data)
67
+ return if @close_scheduled
68
+ buffer = ByteBuffer.wrap(data.to_java_bytes)
69
+ if buffer.remaining() > 0
70
+ @outbound_queue << buffer
71
+ update_events
72
+ end
73
+ end
74
+
75
+ def read_inbound_data(buffer)
76
+ buffer.clear
77
+ raise IOException.new("eof") if @socket.read(buffer) == -1
78
+ buffer.flip
79
+ return if buffer.limit == 0
80
+ String.from_java_bytes(buffer.array[buffer.position...buffer.limit])
81
+ end
82
+
83
+ def write_outbound_data
84
+ until @outbound_queue.empty?
85
+ buffer = @outbound_queue.first
86
+ @socket.write(buffer) if buffer.remaining > 0
87
+
88
+ # Did we consume the whole outbound buffer? If yes,
89
+ # pop it off and keep looping. If no, the outbound network
90
+ # buffers are full, so break out of here.
91
+ if buffer.remaining == 0
92
+ @outbound_queue.shift
93
+ else
94
+ break
95
+ end
96
+ end
97
+
98
+ if @outbound_queue.empty? && !@close_scheduled
99
+ update_events
100
+ end
101
+
102
+ return (@close_scheduled && @outbound_queue.empty?) ? false : true
103
+ end
104
+
105
+ def finish_connecting
106
+ @socket.finish_connect
107
+ @connect_pending = false
108
+ update_events
109
+ return true
110
+ end
111
+
112
+ def schedule_close(after_writing)
113
+ @outbound_queue.clear unless after_writing
114
+
115
+ if @outbound_queue.empty?
116
+ return true
117
+ else
118
+ update_events
119
+ @close_scheduled = true
120
+ return false
121
+ end
122
+ end
123
+
124
+ # TODO: fix these
125
+ def peer_name
126
+ sock = @socket.socket
127
+ [sock.port, sock.inet_address.host_address]
128
+ end
129
+
130
+ def sock_name
131
+ sock = @socket.socket
132
+ [sock.local_port, sock.local_address.host_address]
133
+ end
134
+
135
+ def connect_pending=(value)
136
+ @connect_pending = value
137
+ update_events
138
+ end
139
+
140
+ def update_events
141
+ return unless @channel_key
142
+ events = current_events
143
+ if @channel_key.interest_ops != events
144
+ @channel_key.interest_ops(events)
145
+ end
146
+ end
147
+
148
+ def has_more?
149
+ false
150
+ end
151
+
152
+ def current_events
153
+ if @socket.respond_to?(:accept)
154
+ return SelectionKey::OP_ACCEPT
155
+ end
156
+
157
+ events = 0
158
+
159
+ if @connect_pending
160
+ events |= SelectionKey::OP_CONNECT
161
+ else
162
+ events |= SelectionKey::OP_READ
163
+ events |= SelectionKey::OP_WRITE unless @outbound_queue.empty?
164
+ end
165
+
166
+ return events
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,61 @@
1
+ module ZMachine
2
+ # Creates a one-time timer
3
+ #
4
+ # timer = ZMachine::Timer.new(5) do
5
+ # # this will never fire because we cancel it
6
+ # end
7
+ # timer.cancel
8
+ #
9
+ class Timer
10
+ # Create a new timer that fires after a given number of seconds
11
+ def initialize(interval, callback=nil, &block)
12
+ @signature = ZMachine.add_timer(interval, callback || block)
13
+ end
14
+
15
+ # Cancel the timer
16
+ def cancel
17
+ ZMachine.cancel_timer(@signature)
18
+ end
19
+ end
20
+
21
+ # Creates a periodic timer
22
+ #
23
+ # @example
24
+ # n = 0
25
+ # timer = ZMachine::PeriodicTimer.new(5) do
26
+ # puts "the time is #{Time.now}"
27
+ # timer.cancel if (n+=1) > 5
28
+ # end
29
+ #
30
+ class PeriodicTimer
31
+ # Create a new periodic timer that executes every interval seconds
32
+ def initialize(interval, callback=nil, &block)
33
+ @interval = interval
34
+ @code = callback || block
35
+ @cancelled = false
36
+ @work = method(:fire)
37
+ schedule
38
+ end
39
+
40
+ # Cancel the periodic timer
41
+ def cancel
42
+ @cancelled = true
43
+ end
44
+
45
+ # Fire the timer every interval seconds
46
+ attr_accessor :interval
47
+
48
+ # @private
49
+ def schedule
50
+ ZMachine.add_timer(@interval, @work)
51
+ end
52
+
53
+ # @private
54
+ def fire
55
+ unless @cancelled
56
+ @code.call
57
+ schedule
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,101 @@
1
+ java_import org.zeromq.ZMQ
2
+ java_import org.zeromq.ZMsg
3
+ java_import org.zeromq.ZMQException
4
+
5
+ require 'zmachine/channel'
6
+
7
+ module ZMachine
8
+ class ZMQChannel < Channel
9
+
10
+ attr_reader :port
11
+
12
+ def initialize(type, selector)
13
+ super(selector)
14
+ @socket = ZMachine.context.create_socket(type)
15
+ @socket.linger = 0
16
+ @socket.set_router_mandatory(true) if type == ZMQ::ROUTER
17
+ end
18
+
19
+ def register
20
+ @channel_key ||= @socket.fd.register(@selector, current_events, self)
21
+ end
22
+
23
+ def bind(address)
24
+ @port = @socket.bind(address)
25
+ end
26
+
27
+ def connect(address)
28
+ @socket.connect(address) if address
29
+ end
30
+
31
+ def identity=(value)
32
+ @socket.identity = value.to_java_bytes
33
+ end
34
+
35
+ def close
36
+ if @channel_key
37
+ @channel_key.cancel
38
+ @channel_key = nil
39
+ end
40
+
41
+ @socket.close
42
+ end
43
+
44
+ def send_data(data)
45
+ @outbound_queue << data unless send_msg(data)
46
+ end
47
+
48
+ def send_msg(msg)
49
+ msg.java_send(:send, [org.zeromq.ZMQ::Socket], @socket)
50
+ return true
51
+ rescue ZMQException
52
+ return false
53
+ end
54
+
55
+ def read_inbound_data(buffer)
56
+ return unless has_more?
57
+ ZMsg.recv_msg(@socket)
58
+ end
59
+
60
+ def write_outbound_data
61
+ until @outbound_queue.empty?
62
+ data = @outbound_queue.first
63
+ if send_msg(data)
64
+ @outbound_queue.shift
65
+ else
66
+ break
67
+ end
68
+ end
69
+
70
+ return true
71
+ end
72
+
73
+ def has_more?
74
+ @socket.events & ZMQ::Poller::POLLIN == ZMQ::Poller::POLLIN
75
+ end
76
+
77
+ def can_send?
78
+ @socket.events & ZMQ::Poller::POLLOUT == ZMQ::Poller::POLLOUT
79
+ end
80
+
81
+ def schedule_close(after_writing)
82
+ true
83
+ end
84
+
85
+ # TODO: fix me
86
+ def peer_name
87
+ sock = @socket.socket
88
+ [sock.port, sock.inet_address.host_address]
89
+ end
90
+
91
+ def sock_name
92
+ sock = @socket.socket
93
+ [sock.local_port, sock.local_address.host_address]
94
+ end
95
+
96
+ def current_events
97
+ SelectionKey::OP_READ
98
+ end
99
+
100
+ end
101
+ end