zmachine 0.1.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.
@@ -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