em-zmq-tp10 0.1.7
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.
- 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
|