em-zmq-tp10 0.1.7
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +237 -0
- data/Rakefile +9 -0
- data/em-zmq-tp10.gemspec +23 -0
- data/lib/em-zmq-tp10.rb +1 -0
- data/lib/em/protocols/zmq2.rb +25 -0
- data/lib/em/protocols/zmq2/connection.rb +37 -0
- data/lib/em/protocols/zmq2/dealer.rb +133 -0
- data/lib/em/protocols/zmq2/inproc.rb +147 -0
- data/lib/em/protocols/zmq2/pub_sub.rb +102 -0
- data/lib/em/protocols/zmq2/queue_per_peer.rb +57 -0
- data/lib/em/protocols/zmq2/rep.rb +64 -0
- data/lib/em/protocols/zmq2/req.rb +325 -0
- data/lib/em/protocols/zmq2/router.rb +69 -0
- data/lib/em/protocols/zmq2/socket.rb +236 -0
- data/lib/em/protocols/zmq2/socket_connection.rb +151 -0
- data/lib/em/protocols/zmq2/version.rb +7 -0
- data/lib/em/protocols/zmq_tp10.rb +2 -0
- data/tests/helper.rb +102 -0
- data/tests/run_all.rb +3 -0
- data/tests/test_dealer.rb +237 -0
- data/tests/test_inproc.rb +113 -0
- data/tests/test_pub_sub.rb +271 -0
- data/tests/test_reconnect.rb +64 -0
- data/tests/test_rep.rb +117 -0
- data/tests/test_req.rb +229 -0
- data/tests/test_router.rb +221 -0
- metadata +108 -0
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'em/protocols/zmq2/socket'
|
2
|
+
require 'em/protocols/zmq2/queue_per_peer'
|
3
|
+
module EventMachine
|
4
|
+
module Protocols
|
5
|
+
module Zmq2
|
6
|
+
# ZMQ socket which acts like Router but without outgoing message queueing.
|
7
|
+
# It counts first message string as peer identity when sending message and
|
8
|
+
# prepends socket identity to message on receiving.
|
9
|
+
class PreRouter < Socket
|
10
|
+
def receive_message_and_peer(message, peer_identity) # :nodoc:
|
11
|
+
message.unshift(peer_identity)
|
12
|
+
receive_message(message)
|
13
|
+
end
|
14
|
+
|
15
|
+
def receive_message(message)
|
16
|
+
raise NoMethodError
|
17
|
+
end
|
18
|
+
|
19
|
+
def send_message(message, even_if_busy = false)
|
20
|
+
if connect = choose_peer(message.first, even_if_busy)
|
21
|
+
connect.send_strings(message[1..-1])
|
22
|
+
true
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
# by default chooses peer by peer_identity, but you could wary it
|
28
|
+
def choose_peer(peer_identity, even_if_busy = false)
|
29
|
+
if (connect = @peers[peer_identity]) && !connect.error? &&
|
30
|
+
(even_if_busy || connect.not_too_busy?)
|
31
|
+
connect
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
# ZMQ socket which acts like Router.
|
38
|
+
# It counts first message string as peer identity when sending message and
|
39
|
+
# prepends socket identity to message on receiving.
|
40
|
+
class Router < PreRouter
|
41
|
+
include QueuePerPeer
|
42
|
+
def send_message(message)
|
43
|
+
peer_identity = message.first
|
44
|
+
unless (queue = @queues[peer_identity])
|
45
|
+
if generated_identity?(peer_identity)
|
46
|
+
return false
|
47
|
+
else
|
48
|
+
queue = @queues[peer_identity] = []
|
49
|
+
end
|
50
|
+
end
|
51
|
+
peer = choose_peer(peer_identity)
|
52
|
+
if peer && (queue.empty? || flush_queue(queue, peer)) &&
|
53
|
+
!peer.error? && peer.not_too_busy?
|
54
|
+
peer.send_strings(message[1..-1])
|
55
|
+
true
|
56
|
+
else
|
57
|
+
push_to_queue(queue, message)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
def send_formed_message(peer, from_queue)
|
63
|
+
peer.send_strings(from_queue[1..-1])
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,236 @@
|
|
1
|
+
require 'em/protocols/zmq2/socket_connection'
|
2
|
+
require 'em/protocols/zmq2/inproc'
|
3
|
+
module EM
|
4
|
+
module Protocols
|
5
|
+
module Zmq2
|
6
|
+
# Base class for all ZMQ sockets.
|
7
|
+
# It implements address parsing, binding, connecting and reconnecting
|
8
|
+
# For implementing your own kind of socket you should override at least
|
9
|
+
# #receive_message, and define method which will choose peer from @peers,
|
10
|
+
# and call its peer#send_strings
|
11
|
+
class Socket
|
12
|
+
attr :identity, :hwm
|
13
|
+
attr_accessor :hwm_strategy
|
14
|
+
GENERATED = '%GN%'.freeze
|
15
|
+
|
16
|
+
# Accept options, which are dependend on socket type
|
17
|
+
# Common options are:
|
18
|
+
# [+:identity+] socket identity
|
19
|
+
# [+:hwm+] highwater mark
|
20
|
+
# [+:hwm_strategy+] what to do on send_message when :hwm reached, hwm_strategy could be:
|
21
|
+
# +:drop_last+ - do not accept message for sending - what Zmq does
|
22
|
+
# +:drop_first+ - remove message from queue head and add this message to queue tail
|
23
|
+
#
|
24
|
+
# this class provides convinient method +#push_to_queue+ for default strategy, but
|
25
|
+
# it is up to subclass how to use it.
|
26
|
+
#
|
27
|
+
# Another note concerning highwatermark: EventMachine does not allow precise control on
|
28
|
+
# outgoing data buffer, so that there is a bit more message will be lost, when outgoing
|
29
|
+
# peer disconnected.
|
30
|
+
def initialize(opts = {})
|
31
|
+
@hwm = opts[:hwm] || HWM_INFINITY
|
32
|
+
@hwm_strategy = opts[:hwm_strategy] || :drop_last
|
33
|
+
@identity = opts[:identity] || EMPTY
|
34
|
+
@peers = {}
|
35
|
+
@free_peers = {}
|
36
|
+
@connections = {}
|
37
|
+
@conn_addresses = {}
|
38
|
+
@reconnect_timers = {}
|
39
|
+
@bindings = []
|
40
|
+
@after_writting = nil
|
41
|
+
@uniq_identity = '%GN%aaaaaaaaaaaa' # ~ 100 years to overflow
|
42
|
+
end
|
43
|
+
|
44
|
+
# binding to port
|
45
|
+
# :call-seq:
|
46
|
+
# bind('tcp://host:port') - bind to tcp port
|
47
|
+
# bind('ipc://filename') - bind to unix port
|
48
|
+
def bind(addr)
|
49
|
+
kind, *socket = parse_address(addr)
|
50
|
+
EM.schedule {
|
51
|
+
@bindings << case kind
|
52
|
+
when :tcp, :ipc
|
53
|
+
EM.start_server(*socket, SocketConnection, self)
|
54
|
+
when :inproc
|
55
|
+
InProc.bind(addr, self)
|
56
|
+
end
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
# connect to port
|
61
|
+
# :call-seq:
|
62
|
+
# connect('tcp://host:port') - connect to tcp port
|
63
|
+
# connect('ipc://filename') - connect to unix port
|
64
|
+
def connect(addr)
|
65
|
+
kind, *socket = parse_address(addr)
|
66
|
+
EM.schedule lambda{
|
67
|
+
@reconnect_timers.delete addr
|
68
|
+
unless @conn_addresses[ addr ]
|
69
|
+
connection = case kind
|
70
|
+
when :tcp
|
71
|
+
EM.connect(*socket, SocketConnection, self)
|
72
|
+
when :ipc
|
73
|
+
begin
|
74
|
+
EM.connect_unix_domain(*socket, SocketConnection, self)
|
75
|
+
rescue RuntimeError
|
76
|
+
timer = EM.add_timer(SMALL_TIMEOUT) do
|
77
|
+
connect(addr)
|
78
|
+
end
|
79
|
+
@reconnect_timers[addr] = timer
|
80
|
+
break
|
81
|
+
end
|
82
|
+
when :inproc
|
83
|
+
InProc.connect(addr, self)
|
84
|
+
end
|
85
|
+
@connections[ connection ] = addr
|
86
|
+
@conn_addresses[ addr ] = connection
|
87
|
+
end
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
# :stopdoc:
|
92
|
+
def not_connected(connection)
|
93
|
+
if addr = @connections.delete(connection)
|
94
|
+
@conn_addresses.delete addr
|
95
|
+
timer = EM.add_timer(SMALL_TIMEOUT) do
|
96
|
+
connect(addr)
|
97
|
+
end
|
98
|
+
@reconnect_timers[addr] = timer
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def register_peer(peer_identity, connection)
|
103
|
+
peer_identity = next_uniq_identity if peer_identity.empty?
|
104
|
+
@peers[peer_identity] = connection
|
105
|
+
EM.next_tick{ peer_free(peer_identity, connection) }
|
106
|
+
peer_identity
|
107
|
+
end
|
108
|
+
|
109
|
+
def unregister_peer(peer_identity)
|
110
|
+
@peers.delete peer_identity
|
111
|
+
@free_peers.delete peer_identity
|
112
|
+
if @peers.empty? && @after_writting.respond_to?(:call)
|
113
|
+
@after_writting.call
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# :startdoc:
|
118
|
+
|
119
|
+
# close all connections
|
120
|
+
# if callback is passed, then it will be called after all messages written to sockets
|
121
|
+
def close(cb = nil, &block)
|
122
|
+
@connections.clear
|
123
|
+
@conn_addresses.clear
|
124
|
+
@reconnect_timers.each{|_, timer| EM.cancel_timer(timer)}
|
125
|
+
@after_writting = cb || block
|
126
|
+
flush_all_queue if @after_writting
|
127
|
+
@peers.values.each{|c| c.close_connection(!!@after_writting)}
|
128
|
+
@bindings.each{|c|
|
129
|
+
unless String === c
|
130
|
+
EM.stop_server c
|
131
|
+
else
|
132
|
+
InProc.unbind(c, self)
|
133
|
+
end
|
134
|
+
}
|
135
|
+
end
|
136
|
+
|
137
|
+
# override to make sure all messages are sent before socket closed
|
138
|
+
def flush_all_queue
|
139
|
+
true
|
140
|
+
end
|
141
|
+
|
142
|
+
# stub method for sending message to a socket
|
143
|
+
# note that every socket type should define proper behaviour here
|
144
|
+
# or/and define another useful, semantic clear methods
|
145
|
+
def send_message(message)
|
146
|
+
raise NoMethodError
|
147
|
+
end
|
148
|
+
|
149
|
+
# callback method called with underlied connection when
|
150
|
+
# some message arrives
|
151
|
+
def receive_message_and_peer(message, peer_identity)
|
152
|
+
raise NoMethodError
|
153
|
+
end
|
154
|
+
|
155
|
+
# Change hwm
|
156
|
+
def hwm=(new_hwm)
|
157
|
+
old_hwm, @hwm = @hwm, new_hwm
|
158
|
+
react_on_hwm_decrease if old_hwm > @hwm
|
159
|
+
@hwm
|
160
|
+
end
|
161
|
+
|
162
|
+
# callback method, called when underlying peer is free for writing in
|
163
|
+
# should be used in subclasses for proper reaction on network instability
|
164
|
+
def peer_free(peer_identity, peer) # :doc:
|
165
|
+
@free_peers[peer_identity] = peer
|
166
|
+
end
|
167
|
+
|
168
|
+
private
|
169
|
+
# splits message into envelope and message as defined by ZMQ 2.x
|
170
|
+
def split_message(message) # :doc:
|
171
|
+
i = message.index(EMPTY)
|
172
|
+
[message.slice(0, i), message.slice(i+1, message.size)]
|
173
|
+
end
|
174
|
+
|
175
|
+
# helper method for managing queue concerning @hwm setting
|
176
|
+
def push_to_queue(queue, message = nil) # :doc
|
177
|
+
if queue.size >= @hwm
|
178
|
+
case @hwm_strategy
|
179
|
+
when :drop_last
|
180
|
+
if queue.size > @hwm
|
181
|
+
queue.pop(queue.size - @hwm).each{|message|
|
182
|
+
cancel_message(message)
|
183
|
+
}
|
184
|
+
end
|
185
|
+
false
|
186
|
+
when :drop_first
|
187
|
+
hwm = @hwm - (message ? 1 : 0)
|
188
|
+
queue.shift(queue.size - hwm).each{|earlier|
|
189
|
+
cancel_message(earlier)
|
190
|
+
}
|
191
|
+
queue.push(message.dup) if message
|
192
|
+
true
|
193
|
+
end
|
194
|
+
else
|
195
|
+
queue.push(message.dup) if message
|
196
|
+
true
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
# override to correctly react on hwm decrease
|
201
|
+
def react_on_hwm_decrease # :doc:
|
202
|
+
true
|
203
|
+
end
|
204
|
+
|
205
|
+
# overried if you should react on dropped requests
|
206
|
+
def cancel_message(message) # :doc:
|
207
|
+
true
|
208
|
+
end
|
209
|
+
|
210
|
+
def next_uniq_identity
|
211
|
+
res = @uniq_identity
|
212
|
+
@uniq_identity = res.next
|
213
|
+
res
|
214
|
+
end
|
215
|
+
|
216
|
+
def generated_identity?(id)
|
217
|
+
id.start_with?(GENERATED)
|
218
|
+
end
|
219
|
+
|
220
|
+
def parse_address(addr)
|
221
|
+
case addr
|
222
|
+
when %r{tcp://([^:]+):(\d+)}
|
223
|
+
[:tcp, $1, $2.to_i]
|
224
|
+
when %r{ipc://(.+)}
|
225
|
+
[:ipc, $1]
|
226
|
+
when %r{inproc://(.+)}
|
227
|
+
[:inproc, $1]
|
228
|
+
else
|
229
|
+
raise 'Not supported ZMQ socket kind'
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
require 'em/protocols/zmq2'
|
3
|
+
require 'em/protocols/zmq2/connection'
|
4
|
+
module EventMachine
|
5
|
+
module Protocols
|
6
|
+
module Zmq2
|
7
|
+
# Main implementation peace, heart of protocol
|
8
|
+
module PackString
|
9
|
+
BIG_PACK = 'CNNCa*'.freeze
|
10
|
+
SMALL_PACK = 'CCa*'.freeze
|
11
|
+
def pack_string(string, more = false)
|
12
|
+
bytesize = string.bytesize + 1
|
13
|
+
if bytesize <= 254
|
14
|
+
[bytesize, more ? 1 : 0, string].pack(SMALL_PACK)
|
15
|
+
else
|
16
|
+
[255, 0, bytesize, more ? 1 : 0, string].pack(BIG_PACK)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def prepare_message(message)
|
21
|
+
if String === message
|
22
|
+
pack_string(message, false)
|
23
|
+
else
|
24
|
+
message = Array(message)
|
25
|
+
buffer = ''
|
26
|
+
i = 0
|
27
|
+
last = message.size - 1
|
28
|
+
while i < last
|
29
|
+
buffer << pack_string(message[i].to_s, true)
|
30
|
+
i += 1
|
31
|
+
end
|
32
|
+
buffer << pack_string(message[last].to_s, false)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Heavy duty worker class
|
38
|
+
# Implements ZMTP1.0 - ZMQ2.x transport protocol
|
39
|
+
#
|
40
|
+
# It calls following callbacks
|
41
|
+
#
|
42
|
+
# It is not for end user usage
|
43
|
+
class SocketConnection < EM::Connection
|
44
|
+
include ConnectionMixin
|
45
|
+
# :stopdoc:
|
46
|
+
def initialize(socket)
|
47
|
+
@socket = socket
|
48
|
+
@recv_buffer = ''
|
49
|
+
@recv_frames = [[]]
|
50
|
+
end
|
51
|
+
|
52
|
+
def unbind(err)
|
53
|
+
if @peer_identity
|
54
|
+
@socket.unregister_peer(@peer_identity)
|
55
|
+
end
|
56
|
+
@socket.not_connected(self)
|
57
|
+
end
|
58
|
+
|
59
|
+
# use watching on outbound queue when possible
|
60
|
+
# rely on https://github.com/eventmachine/eventmachine/pull/317 if were accepted
|
61
|
+
# or on https://github.com/funny-falcon/eventmachine/tree/sent_data
|
62
|
+
# use timers otherwise
|
63
|
+
def sent_data
|
64
|
+
@socket.peer_free(@peer_identity, self) if not_too_busy?
|
65
|
+
end
|
66
|
+
|
67
|
+
if method_defined?(:outbound_data_count)
|
68
|
+
def _not_too_busy?
|
69
|
+
outbound_data_count < 32
|
70
|
+
end
|
71
|
+
else
|
72
|
+
def _not_too_busy?
|
73
|
+
get_outbound_data_size < 2048
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
if method_defined?(:notify_sent_data=)
|
78
|
+
alias notify_when_free= notify_sent_data=
|
79
|
+
else
|
80
|
+
def notify_when_free=(v)
|
81
|
+
if v
|
82
|
+
@when_free_timer ||= EM.add_timer(SMALL_TIMEOUT) do
|
83
|
+
@when_free_timer = nil
|
84
|
+
sent_data
|
85
|
+
end
|
86
|
+
elsif @when_free_timer
|
87
|
+
EM.cancel_timer @when_free_timer
|
88
|
+
@when_free_timer = nil
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def not_too_busy?
|
94
|
+
free = _not_too_busy?
|
95
|
+
self.notify_when_free = !free
|
96
|
+
free
|
97
|
+
end
|
98
|
+
|
99
|
+
def receive_data(data)
|
100
|
+
parse_frames(data)
|
101
|
+
while message = pop_message
|
102
|
+
receive_strings(message)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def pop_message
|
107
|
+
if @recv_frames.size > 1
|
108
|
+
@recv_frames.shift
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
FF = "\xff".freeze
|
113
|
+
BIG_UNPACK = 'CNNC'.freeze
|
114
|
+
SMALL_UNPACK = 'CC'.freeze
|
115
|
+
def parse_frames(data)
|
116
|
+
unless @recv_buffer.empty?
|
117
|
+
data = @recv_buffer << data
|
118
|
+
end
|
119
|
+
while data.bytesize >= 2
|
120
|
+
if data.start_with?(FF)
|
121
|
+
break if data.bytesize < 10
|
122
|
+
_, _, length, more = data.unpack(BIG_UNPACK)
|
123
|
+
start_at = 10
|
124
|
+
else
|
125
|
+
length = data.getbyte(0)
|
126
|
+
more = data.getbyte(1)
|
127
|
+
start_at = 2
|
128
|
+
end
|
129
|
+
length -= 1
|
130
|
+
break if data.bytesize < start_at + length
|
131
|
+
str = data.byteslice(start_at, length)
|
132
|
+
data[0, start_at + length] = EMPTY
|
133
|
+
@recv_frames.last << str
|
134
|
+
@recv_frames << [] if more & 1 == 0
|
135
|
+
end
|
136
|
+
@recv_buffer = data
|
137
|
+
end
|
138
|
+
|
139
|
+
include PackString
|
140
|
+
|
141
|
+
def send_strings(strings)
|
142
|
+
send_data prepare_message(strings)
|
143
|
+
end
|
144
|
+
|
145
|
+
def send_strings_or_prepared(strings, prepared)
|
146
|
+
send_data prepared
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|