omq 0.23.0 → 0.26.2
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +144 -3
- data/README.md +23 -7
- data/lib/omq/client_server.rb +1 -1
- data/lib/omq/engine/connection_lifecycle.rb +12 -6
- data/lib/omq/engine/heartbeat.rb +1 -1
- data/lib/omq/engine/recv_pump.rb +29 -16
- data/lib/omq/engine.rb +6 -5
- data/lib/omq/ffi/engine.rb +646 -0
- data/lib/omq/ffi/libzmq.rb +134 -0
- data/lib/omq/ffi.rb +12 -0
- data/lib/omq/peer.rb +1 -1
- data/lib/omq/radio_dish.rb +1 -1
- data/lib/omq/readable.rb +5 -1
- data/lib/omq/routing/channel.rb +4 -4
- data/lib/omq/routing/client.rb +2 -2
- data/lib/omq/routing/conn_send_pump.rb +1 -1
- data/lib/omq/routing/dealer.rb +2 -2
- data/lib/omq/routing/dish.rb +2 -2
- data/lib/omq/routing/fan_out.rb +7 -7
- data/lib/omq/routing/gather.rb +2 -2
- data/lib/omq/routing/pair.rb +4 -4
- data/lib/omq/routing/peer.rb +2 -2
- data/lib/omq/routing/pub.rb +2 -2
- data/lib/omq/routing/pull.rb +2 -2
- data/lib/omq/routing/push.rb +3 -3
- data/lib/omq/routing/radio.rb +2 -2
- data/lib/omq/routing/rep.rb +2 -2
- data/lib/omq/routing/req.rb +2 -2
- data/lib/omq/routing/round_robin.rb +4 -4
- data/lib/omq/routing/router.rb +2 -2
- data/lib/omq/routing/scatter.rb +4 -5
- data/lib/omq/routing/server.rb +2 -2
- data/lib/omq/routing/sub.rb +2 -2
- data/lib/omq/routing/xpub.rb +2 -2
- data/lib/omq/routing/xsub.rb +2 -2
- data/lib/omq/socket.rb +2 -1
- data/lib/omq/transport/inproc/{direct_pipe.rb → pipe.rb} +22 -9
- data/lib/omq/transport/inproc.rb +26 -14
- data/lib/omq/transport/udp.rb +1 -1
- data/lib/omq/version.rb +1 -1
- data/lib/omq/writable.rb +32 -43
- metadata +5 -2
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "async"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module OMQ
|
|
7
|
+
module FFI
|
|
8
|
+
# FFI Engine — wraps a libzmq socket to implement the OMQ Engine contract.
|
|
9
|
+
#
|
|
10
|
+
# A dedicated I/O thread owns the zmq_socket exclusively (libzmq sockets
|
|
11
|
+
# are not thread-safe). Send and recv flow through queues, with an IO pipe
|
|
12
|
+
# to wake the Async fiber scheduler.
|
|
13
|
+
#
|
|
14
|
+
class Engine
|
|
15
|
+
L = Libzmq
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# @return [Options] socket options
|
|
19
|
+
attr_reader :options
|
|
20
|
+
# @return [Array] active connections
|
|
21
|
+
attr_reader :connections
|
|
22
|
+
# @return [RoutingStub] subscription/group routing interface
|
|
23
|
+
attr_reader :routing
|
|
24
|
+
# @return [Async::Promise] resolved when the first peer connects
|
|
25
|
+
attr_reader :peer_connected
|
|
26
|
+
# @return [Async::Promise] resolved when all peers have disconnected
|
|
27
|
+
attr_reader :all_peers_gone
|
|
28
|
+
# @return [Async::Task, nil] root of the engine's task tree
|
|
29
|
+
attr_reader :parent_task
|
|
30
|
+
# @return [Boolean] true when the engine's parent task lives on the
|
|
31
|
+
# shared {OMQ::Reactor} IO thread (i.e. not created under an
|
|
32
|
+
# Async task). Writable/Readable check this to pick the fast path.
|
|
33
|
+
attr_reader :on_io_thread
|
|
34
|
+
alias on_io_thread? on_io_thread
|
|
35
|
+
# @param value [Boolean] enables or disables automatic reconnection
|
|
36
|
+
attr_writer :reconnect_enabled
|
|
37
|
+
# @note Monitor events are not yet emitted by the FFI backend; these
|
|
38
|
+
# writers exist so Socket#monitor can attach without raising. Wiring
|
|
39
|
+
# libzmq's zmq_socket_monitor is a TODO.
|
|
40
|
+
attr_writer :monitor_queue, :verbose_monitor
|
|
41
|
+
|
|
42
|
+
# Routing stub that delegates subscribe/unsubscribe/join/leave to
|
|
43
|
+
# libzmq socket options via the I/O thread.
|
|
44
|
+
#
|
|
45
|
+
class RoutingStub
|
|
46
|
+
# @return [Async::Promise] resolved when a subscriber joins
|
|
47
|
+
attr_reader :subscriber_joined
|
|
48
|
+
|
|
49
|
+
# @param engine [Engine] the parent engine instance
|
|
50
|
+
def initialize(engine)
|
|
51
|
+
@engine = engine
|
|
52
|
+
@subscriber_joined = Async::Promise.new
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# Subscribes to messages matching the given prefix.
|
|
57
|
+
#
|
|
58
|
+
# @param prefix [String] subscription prefix
|
|
59
|
+
# @return [void]
|
|
60
|
+
def subscribe(prefix)
|
|
61
|
+
@engine.send_cmd(:subscribe, prefix.b)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# Removes a subscription for the given prefix.
|
|
66
|
+
#
|
|
67
|
+
# @param prefix [String] subscription prefix to remove
|
|
68
|
+
# @return [void]
|
|
69
|
+
def unsubscribe(prefix)
|
|
70
|
+
@engine.send_cmd(:unsubscribe, prefix.b)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# Joins a DISH group for receiving RADIO messages.
|
|
75
|
+
#
|
|
76
|
+
# @param group [String] group name
|
|
77
|
+
# @return [void]
|
|
78
|
+
def join(group)
|
|
79
|
+
@engine.send_cmd(:join, group)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# Leaves a DISH group.
|
|
84
|
+
#
|
|
85
|
+
# @param group [String] group name
|
|
86
|
+
# @return [void]
|
|
87
|
+
def leave(group)
|
|
88
|
+
@engine.send_cmd(:leave, group)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# Maps an OMQ +linger+ value (seconds, or +nil+/+Float::INFINITY+
|
|
94
|
+
# for "wait forever") to libzmq's ZMQ_LINGER int milliseconds
|
|
95
|
+
# (-1 = infinite, 0 = drop, N = N ms).
|
|
96
|
+
#
|
|
97
|
+
# @param linger [Numeric, nil]
|
|
98
|
+
# @return [Integer]
|
|
99
|
+
#
|
|
100
|
+
def self.linger_to_zmq_ms(linger)
|
|
101
|
+
return -1 if linger.nil? || linger == Float::INFINITY
|
|
102
|
+
(linger * 1000).to_i
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# @param socket_type [Symbol] e.g. :REQ, :PAIR
|
|
107
|
+
# @param options [Options]
|
|
108
|
+
#
|
|
109
|
+
def initialize(socket_type, options)
|
|
110
|
+
@socket_type = socket_type
|
|
111
|
+
@options = options
|
|
112
|
+
@peer_connected = Async::Promise.new
|
|
113
|
+
@all_peers_gone = Async::Promise.new
|
|
114
|
+
@connections = []
|
|
115
|
+
@closed = false
|
|
116
|
+
@parent_task = nil
|
|
117
|
+
@on_io_thread = false
|
|
118
|
+
|
|
119
|
+
@zmq_socket = L.zmq_socket(OMQ::FFI.context, L::SOCKET_TYPES.fetch(@socket_type))
|
|
120
|
+
raise "zmq_socket failed: #{L.zmq_strerror(L.zmq_errno)}" if @zmq_socket.null?
|
|
121
|
+
|
|
122
|
+
apply_options
|
|
123
|
+
|
|
124
|
+
@routing = RoutingStub.new(self)
|
|
125
|
+
|
|
126
|
+
# Queues for cross-thread communication
|
|
127
|
+
@send_queue = Thread::Queue.new # main → io thread
|
|
128
|
+
@recv_queue = Thread::Queue.new # io thread → main
|
|
129
|
+
@cmd_queue = Thread::Queue.new # control commands → io thread
|
|
130
|
+
|
|
131
|
+
# Signal pipe: io thread → Async fiber (message received)
|
|
132
|
+
@recv_signal_r, @recv_signal_w = IO.pipe
|
|
133
|
+
# Wake pipe: main thread → io thread (send/cmd enqueued)
|
|
134
|
+
@wake_r, @wake_w = IO.pipe
|
|
135
|
+
|
|
136
|
+
@io_thread = nil
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# --- Socket lifecycle ---
|
|
141
|
+
|
|
142
|
+
# Binds the socket to the given endpoint.
|
|
143
|
+
#
|
|
144
|
+
# @param endpoint [String] ZMQ endpoint URL (e.g. "tcp://*:5555")
|
|
145
|
+
# @return [URI::Generic] resolved endpoint URI (with auto-selected port for "tcp://host:0")
|
|
146
|
+
def bind(endpoint)
|
|
147
|
+
sync_identity
|
|
148
|
+
send_cmd(:bind, endpoint)
|
|
149
|
+
resolved = get_string_option(L::ZMQ_LAST_ENDPOINT)
|
|
150
|
+
@connections << :ffi
|
|
151
|
+
@peer_connected.resolve(:ffi) unless @peer_connected.resolved?
|
|
152
|
+
URI.parse(resolved)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# Connects the socket to the given endpoint.
|
|
157
|
+
#
|
|
158
|
+
# @param endpoint [String] ZMQ endpoint URL
|
|
159
|
+
# @return [URI::Generic] parsed endpoint URI
|
|
160
|
+
def connect(endpoint)
|
|
161
|
+
sync_identity
|
|
162
|
+
send_cmd(:connect, endpoint)
|
|
163
|
+
@connections << :ffi
|
|
164
|
+
@peer_connected.resolve(:ffi) unless @peer_connected.resolved?
|
|
165
|
+
URI.parse(endpoint)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# Disconnects from the given endpoint.
|
|
170
|
+
#
|
|
171
|
+
# @param endpoint [String] ZMQ endpoint URL
|
|
172
|
+
# @return [void]
|
|
173
|
+
def disconnect(endpoint)
|
|
174
|
+
send_cmd(:disconnect, endpoint)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# Unbinds from the given endpoint.
|
|
179
|
+
#
|
|
180
|
+
# @param endpoint [String] ZMQ endpoint URL
|
|
181
|
+
# @return [void]
|
|
182
|
+
def unbind(endpoint)
|
|
183
|
+
send_cmd(:unbind, endpoint)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# Subscribes to a topic prefix (SUB/XSUB). Delegates to the routing
|
|
188
|
+
# stub for API parity with the pure-Ruby Engine.
|
|
189
|
+
#
|
|
190
|
+
# @param prefix [String]
|
|
191
|
+
# @return [void]
|
|
192
|
+
def subscribe(prefix)
|
|
193
|
+
@routing.subscribe(prefix)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# Unsubscribes from a topic prefix (SUB/XSUB).
|
|
198
|
+
#
|
|
199
|
+
# @param prefix [String]
|
|
200
|
+
# @return [void]
|
|
201
|
+
def unsubscribe(prefix)
|
|
202
|
+
@routing.unsubscribe(prefix)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# @return [Async::Promise] resolved when a subscriber joins (PUB/XPUB).
|
|
207
|
+
def subscriber_joined
|
|
208
|
+
@routing.subscriber_joined
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
# Closes the socket and shuts down the I/O thread.
|
|
213
|
+
#
|
|
214
|
+
# Honors `options.linger`:
|
|
215
|
+
# nil → wait forever for Ruby-side queue to drain into libzmq
|
|
216
|
+
# and for libzmq's own LINGER to flush to the network
|
|
217
|
+
# 0 → drop anything not yet in libzmq's kernel buffers, close fast
|
|
218
|
+
# N → up to N seconds for drain + N + 1s grace for join
|
|
219
|
+
#
|
|
220
|
+
# @return [void]
|
|
221
|
+
def close
|
|
222
|
+
return if @closed
|
|
223
|
+
@closed = true
|
|
224
|
+
if @io_thread
|
|
225
|
+
@cmd_queue.push([:stop])
|
|
226
|
+
wake_io_thread
|
|
227
|
+
linger = @options.linger
|
|
228
|
+
if linger.nil?
|
|
229
|
+
@io_thread.join
|
|
230
|
+
elsif linger.zero?
|
|
231
|
+
@io_thread.join(0.5) # fast path: zmq_close is non-blocking with LINGER=0
|
|
232
|
+
else
|
|
233
|
+
@io_thread.join(linger + 1.0)
|
|
234
|
+
end
|
|
235
|
+
@io_thread.kill if @io_thread.alive? # hard stop if deadline exceeded
|
|
236
|
+
else
|
|
237
|
+
# IO thread never started — close socket directly
|
|
238
|
+
L.zmq_close(@zmq_socket)
|
|
239
|
+
end
|
|
240
|
+
@recv_signal_r&.close rescue nil
|
|
241
|
+
@recv_signal_w&.close rescue nil
|
|
242
|
+
@wake_r&.close rescue nil
|
|
243
|
+
@wake_w&.close rescue nil
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
# Captures the current Async task as the parent for I/O scheduling.
|
|
248
|
+
# +parent:+ is accepted for API compatibility with the pure-Ruby
|
|
249
|
+
# engine but has no effect: the FFI backend runs its own I/O
|
|
250
|
+
# thread and doesn't participate in the Async barrier tree.
|
|
251
|
+
#
|
|
252
|
+
# @return [void]
|
|
253
|
+
def capture_parent_task(parent: nil)
|
|
254
|
+
return if @parent_task
|
|
255
|
+
if parent
|
|
256
|
+
@parent_task = parent
|
|
257
|
+
elsif Async::Task.current?
|
|
258
|
+
@parent_task = Async::Task.current
|
|
259
|
+
else
|
|
260
|
+
@parent_task = Reactor.root_task
|
|
261
|
+
@on_io_thread = true
|
|
262
|
+
Reactor.track_linger(@options.linger)
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
# --- Send ---
|
|
268
|
+
|
|
269
|
+
# Enqueues a multipart message for sending via the I/O thread.
|
|
270
|
+
#
|
|
271
|
+
# @param parts [Array<String>] message frames
|
|
272
|
+
# @return [void]
|
|
273
|
+
def enqueue_send(parts)
|
|
274
|
+
ensure_io_thread
|
|
275
|
+
@send_queue.push(parts)
|
|
276
|
+
wake_io_thread
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
# --- Recv ---
|
|
281
|
+
|
|
282
|
+
# Dequeues the next received message, blocking until one is available.
|
|
283
|
+
#
|
|
284
|
+
# @return [Array<String>] multipart message
|
|
285
|
+
def dequeue_recv
|
|
286
|
+
ensure_io_thread
|
|
287
|
+
wait_for_message
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
# Pushes a nil sentinel into the recv queue to unblock a waiting consumer.
|
|
292
|
+
#
|
|
293
|
+
# @return [void]
|
|
294
|
+
def dequeue_recv_sentinel
|
|
295
|
+
@recv_queue.push(nil)
|
|
296
|
+
@recv_signal_w.write_nonblock(".", exception: false) rescue nil
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
# Send a control command to the I/O thread.
|
|
301
|
+
# @api private
|
|
302
|
+
#
|
|
303
|
+
def send_cmd(cmd, *args)
|
|
304
|
+
ensure_io_thread
|
|
305
|
+
result = Thread::Queue.new
|
|
306
|
+
@cmd_queue.push([cmd, args, result])
|
|
307
|
+
wake_io_thread
|
|
308
|
+
r = result.pop
|
|
309
|
+
raise r if r.is_a?(Exception)
|
|
310
|
+
r
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
# Wakes the I/O thread via the internal pipe.
|
|
315
|
+
#
|
|
316
|
+
# @return [void]
|
|
317
|
+
def wake_io_thread
|
|
318
|
+
@wake_w.write_nonblock(".", exception: false)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
private
|
|
322
|
+
|
|
323
|
+
# Waits for a message from the I/O thread's recv queue.
|
|
324
|
+
# Uses the signal pipe so Async can yield the fiber.
|
|
325
|
+
#
|
|
326
|
+
def wait_for_message
|
|
327
|
+
loop do
|
|
328
|
+
begin
|
|
329
|
+
return @recv_queue.pop(true)
|
|
330
|
+
rescue ThreadError
|
|
331
|
+
# empty
|
|
332
|
+
end
|
|
333
|
+
@recv_signal_r.wait_readable
|
|
334
|
+
@recv_signal_r.read_nonblock(256, exception: false)
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def ensure_io_thread
|
|
340
|
+
return if @io_thread
|
|
341
|
+
@io_thread = Thread.new { io_loop }
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
# The I/O loop runs on a dedicated thread. It owns the zmq_socket
|
|
346
|
+
# exclusively and processes commands, sends, and recvs.
|
|
347
|
+
#
|
|
348
|
+
def io_loop
|
|
349
|
+
zmq_fd_io = IO.for_fd(get_zmq_fd, autoclose: false)
|
|
350
|
+
|
|
351
|
+
loop do
|
|
352
|
+
drain_cmds or break
|
|
353
|
+
drain_sends
|
|
354
|
+
try_recv
|
|
355
|
+
|
|
356
|
+
# Block until ZMQ or wake pipe has activity.
|
|
357
|
+
IO.select([zmq_fd_io, @wake_r], nil, nil, 0.1)
|
|
358
|
+
@wake_r.read_nonblock(4096, exception: false)
|
|
359
|
+
end
|
|
360
|
+
rescue
|
|
361
|
+
# Thread exit
|
|
362
|
+
ensure
|
|
363
|
+
# Drain Ruby-side send queue into libzmq, bounded by linger deadline.
|
|
364
|
+
# Then re-apply current linger to libzmq (user may have changed it
|
|
365
|
+
# after apply_options ran in initialize) and zmq_close uses it to
|
|
366
|
+
# flush libzmq's own queue to TCP.
|
|
367
|
+
drain_sends_with_deadline(zmq_fd_io, shutdown_deadline) rescue nil
|
|
368
|
+
set_int_option(L::ZMQ_LINGER, Engine.linger_to_zmq_ms(@options.linger)) rescue nil
|
|
369
|
+
zmq_fd_io&.close rescue nil
|
|
370
|
+
L.zmq_close(@zmq_socket)
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
# Returns a monotonic deadline for the Ruby-side drain phase, or nil
|
|
375
|
+
# for infinite, or the current clock for "drop immediately".
|
|
376
|
+
#
|
|
377
|
+
def shutdown_deadline
|
|
378
|
+
linger = @options.linger
|
|
379
|
+
return nil if linger.nil?
|
|
380
|
+
now = Async::Clock.now
|
|
381
|
+
return now if linger.zero?
|
|
382
|
+
now + linger
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
# Retries drain_sends with IO.select until either the Ruby-side queue
|
|
387
|
+
# is empty or the deadline is hit. nil deadline = wait forever.
|
|
388
|
+
#
|
|
389
|
+
def drain_sends_with_deadline(zmq_fd_io, deadline)
|
|
390
|
+
loop do
|
|
391
|
+
drain_sends
|
|
392
|
+
break if @pending_send.nil? && @send_queue.empty?
|
|
393
|
+
if deadline
|
|
394
|
+
remaining = deadline - Async::Clock.now
|
|
395
|
+
break if remaining <= 0
|
|
396
|
+
IO.select([zmq_fd_io], nil, nil, [remaining, 0.1].min)
|
|
397
|
+
else
|
|
398
|
+
IO.select([zmq_fd_io], nil, nil, 0.1)
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def zmq_has_events?
|
|
405
|
+
@events_buf ||= ::FFI::MemoryPointer.new(:int)
|
|
406
|
+
@events_len ||= ::FFI::MemoryPointer.new(:size_t).tap { |p| p.write(:size_t, ::FFI.type_size(:int)) }
|
|
407
|
+
L.zmq_getsockopt(@zmq_socket, L::ZMQ_EVENTS, @events_buf, @events_len)
|
|
408
|
+
@events_buf.read_int != 0
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def drain_cmds
|
|
413
|
+
loop do
|
|
414
|
+
begin
|
|
415
|
+
cmd = @cmd_queue.pop(true)
|
|
416
|
+
rescue ThreadError
|
|
417
|
+
return true # queue empty, continue
|
|
418
|
+
end
|
|
419
|
+
return false unless process_cmd(cmd)
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def process_cmd(cmd)
|
|
425
|
+
name, args, result = cmd
|
|
426
|
+
case name
|
|
427
|
+
when :stop
|
|
428
|
+
result&.push(nil)
|
|
429
|
+
return false
|
|
430
|
+
when :bind
|
|
431
|
+
rc = L.zmq_bind(@zmq_socket, args[0])
|
|
432
|
+
result&.push(rc >= 0 ? nil : syscall_error)
|
|
433
|
+
when :connect
|
|
434
|
+
rc = L.zmq_connect(@zmq_socket, args[0])
|
|
435
|
+
result&.push(rc >= 0 ? nil : syscall_error)
|
|
436
|
+
when :disconnect
|
|
437
|
+
rc = L.zmq_disconnect(@zmq_socket, args[0])
|
|
438
|
+
result&.push(rc >= 0 ? nil : syscall_error)
|
|
439
|
+
when :unbind
|
|
440
|
+
rc = L.zmq_unbind(@zmq_socket, args[0])
|
|
441
|
+
result&.push(rc >= 0 ? nil : syscall_error)
|
|
442
|
+
when :set_identity
|
|
443
|
+
set_bytes_option(L::ZMQ_IDENTITY, args[0])
|
|
444
|
+
result&.push(nil)
|
|
445
|
+
when :subscribe
|
|
446
|
+
set_bytes_option(L::ZMQ_SUBSCRIBE, args[0])
|
|
447
|
+
result&.push(nil)
|
|
448
|
+
when :unsubscribe
|
|
449
|
+
set_bytes_option(L::ZMQ_UNSUBSCRIBE, args[0])
|
|
450
|
+
result&.push(nil)
|
|
451
|
+
when :join
|
|
452
|
+
rc = L.respond_to?(:zmq_join) ? L.zmq_join(@zmq_socket, args[0]) : -1
|
|
453
|
+
result&.push(rc >= 0 ? nil : RuntimeError.new("zmq_join not available"))
|
|
454
|
+
when :leave
|
|
455
|
+
rc = L.respond_to?(:zmq_leave) ? L.zmq_leave(@zmq_socket, args[0]) : -1
|
|
456
|
+
result&.push(rc >= 0 ? nil : RuntimeError.new("zmq_leave not available"))
|
|
457
|
+
when :drain_send
|
|
458
|
+
# handled in drain_sends
|
|
459
|
+
result&.push(nil)
|
|
460
|
+
end
|
|
461
|
+
true
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def try_recv
|
|
466
|
+
loop do
|
|
467
|
+
parts = recv_multipart_nonblock
|
|
468
|
+
break unless parts
|
|
469
|
+
@recv_queue.push(parts.freeze)
|
|
470
|
+
@recv_signal_w.write_nonblock(".", exception: false)
|
|
471
|
+
end
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def drain_sends
|
|
476
|
+
@pending_send ||= nil
|
|
477
|
+
loop do
|
|
478
|
+
parts = @pending_send || begin
|
|
479
|
+
@send_queue.pop(true)
|
|
480
|
+
rescue ThreadError
|
|
481
|
+
break
|
|
482
|
+
end
|
|
483
|
+
if send_multipart_nonblock(parts)
|
|
484
|
+
@pending_send = nil
|
|
485
|
+
else
|
|
486
|
+
@pending_send = parts # retry next cycle (HWM reached)
|
|
487
|
+
break
|
|
488
|
+
end
|
|
489
|
+
end
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
# Returns true if fully sent, false if would block (HWM).
|
|
494
|
+
#
|
|
495
|
+
def send_multipart_nonblock(parts)
|
|
496
|
+
parts.each_with_index do |part, i|
|
|
497
|
+
flags = L::ZMQ_DONTWAIT
|
|
498
|
+
flags |= L::ZMQ_SNDMORE if i < parts.size - 1
|
|
499
|
+
msg = L.alloc_msg
|
|
500
|
+
L.zmq_msg_init_size(msg, part.bytesize)
|
|
501
|
+
L.zmq_msg_data(msg).write_bytes(part)
|
|
502
|
+
rc = L.zmq_msg_send(msg, @zmq_socket, flags)
|
|
503
|
+
if rc < 0
|
|
504
|
+
L.zmq_msg_close(msg)
|
|
505
|
+
return false # EAGAIN — would block
|
|
506
|
+
end
|
|
507
|
+
end
|
|
508
|
+
true
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def recv_multipart_nonblock
|
|
513
|
+
parts = []
|
|
514
|
+
loop do
|
|
515
|
+
msg = L.alloc_msg
|
|
516
|
+
L.zmq_msg_init(msg)
|
|
517
|
+
rc = L.zmq_msg_recv(msg, @zmq_socket, L::ZMQ_DONTWAIT)
|
|
518
|
+
if rc < 0
|
|
519
|
+
L.zmq_msg_close(msg)
|
|
520
|
+
return parts.empty? ? nil : parts # EAGAIN = no more data
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
size = L.zmq_msg_size(msg)
|
|
524
|
+
data = L.zmq_msg_data(msg).read_bytes(size)
|
|
525
|
+
L.zmq_msg_close(msg)
|
|
526
|
+
parts << data.freeze
|
|
527
|
+
|
|
528
|
+
break unless rcvmore?
|
|
529
|
+
end
|
|
530
|
+
parts.empty? ? nil : parts
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def rcvmore?
|
|
535
|
+
buf = ::FFI::MemoryPointer.new(:int)
|
|
536
|
+
len = ::FFI::MemoryPointer.new(:size_t)
|
|
537
|
+
len.write(:size_t, ::FFI.type_size(:int))
|
|
538
|
+
L.zmq_getsockopt(@zmq_socket, L::ZMQ_RCVMORE, buf, len)
|
|
539
|
+
buf.read_int != 0
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def get_zmq_fd
|
|
544
|
+
buf = ::FFI::MemoryPointer.new(:int)
|
|
545
|
+
len = ::FFI::MemoryPointer.new(:size_t)
|
|
546
|
+
len.write(:size_t, ::FFI.type_size(:int))
|
|
547
|
+
L.zmq_getsockopt(@zmq_socket, L::ZMQ_FD, buf, len)
|
|
548
|
+
buf.read_int
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
# Re-syncs identity to libzmq (user may set it after construction).
|
|
553
|
+
#
|
|
554
|
+
def sync_identity
|
|
555
|
+
id = @options.identity
|
|
556
|
+
if id && !id.empty?
|
|
557
|
+
send_cmd(:set_identity, id)
|
|
558
|
+
end
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def apply_options
|
|
563
|
+
set_int_option(L::ZMQ_SNDHWM, @options.send_hwm)
|
|
564
|
+
set_int_option(L::ZMQ_RCVHWM, @options.recv_hwm)
|
|
565
|
+
set_int_option(L::ZMQ_LINGER, Engine.linger_to_zmq_ms(@options.linger))
|
|
566
|
+
set_int_option(L::ZMQ_CONFLATE, @options.conflate ? 1 : 0)
|
|
567
|
+
|
|
568
|
+
if @options.identity && !@options.identity.empty?
|
|
569
|
+
set_bytes_option(L::ZMQ_IDENTITY, @options.identity)
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
if @options.max_message_size
|
|
573
|
+
set_int64_option(L::ZMQ_MAXMSGSIZE, @options.max_message_size)
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
if @options.reconnect_interval
|
|
577
|
+
ivl = @options.reconnect_interval
|
|
578
|
+
if ivl.is_a?(Range)
|
|
579
|
+
set_int_option(L::ZMQ_RECONNECT_IVL, (ivl.begin * 1000).to_i)
|
|
580
|
+
set_int_option(L::ZMQ_RECONNECT_IVL_MAX, (ivl.end * 1000).to_i)
|
|
581
|
+
else
|
|
582
|
+
set_int_option(L::ZMQ_RECONNECT_IVL, (ivl * 1000).to_i)
|
|
583
|
+
end
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
set_int_option(L::ZMQ_ROUTER_MANDATORY, 1) if @options.router_mandatory
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def set_int_option(opt, value)
|
|
591
|
+
buf = ::FFI::MemoryPointer.new(:int)
|
|
592
|
+
buf.write_int(value)
|
|
593
|
+
L.zmq_setsockopt(@zmq_socket, opt, buf, ::FFI.type_size(:int))
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def set_int64_option(opt, value)
|
|
598
|
+
buf = ::FFI::MemoryPointer.new(:int64)
|
|
599
|
+
buf.write_int64(value)
|
|
600
|
+
L.zmq_setsockopt(@zmq_socket, opt, buf, ::FFI.type_size(:int64))
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def set_bytes_option(opt, value)
|
|
605
|
+
buf = ::FFI::MemoryPointer.from_string(value)
|
|
606
|
+
L.zmq_setsockopt(@zmq_socket, opt, buf, value.bytesize)
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def get_string_option(opt)
|
|
611
|
+
buf = ::FFI::MemoryPointer.new(:char, 256)
|
|
612
|
+
len = ::FFI::MemoryPointer.new(:size_t)
|
|
613
|
+
len.write(:size_t, 256)
|
|
614
|
+
L.check!(L.zmq_getsockopt(@zmq_socket, opt, buf, len), "zmq_getsockopt")
|
|
615
|
+
buf.read_string(len.read(:size_t) - 1)
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
# Builds an Errno::XXX exception from the current zmq_errno so callers
|
|
620
|
+
# can rescue the same classes they would from the pure-Ruby backend
|
|
621
|
+
# (e.g. `Errno::EADDRINUSE`, `Errno::ECONNREFUSED`). Falls back to a
|
|
622
|
+
# plain SystemCallError when the errno is libzmq-specific.
|
|
623
|
+
#
|
|
624
|
+
def syscall_error
|
|
625
|
+
errno = L.zmq_errno
|
|
626
|
+
SystemCallError.new(L.zmq_strerror(errno), errno)
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
# Returns the shared ZMQ context (one per process, lazily initialized).
|
|
634
|
+
#
|
|
635
|
+
# @return [FFI::Pointer] zmq context pointer
|
|
636
|
+
def self.context
|
|
637
|
+
@context ||= Libzmq.zmq_ctx_new.tap do |ctx|
|
|
638
|
+
raise "zmq_ctx_new failed" if ctx.null?
|
|
639
|
+
at_exit do
|
|
640
|
+
Libzmq.zmq_ctx_shutdown(ctx) rescue nil
|
|
641
|
+
Libzmq.zmq_ctx_term(ctx) rescue nil
|
|
642
|
+
end
|
|
643
|
+
end
|
|
644
|
+
end
|
|
645
|
+
end
|
|
646
|
+
end
|