omq-ffi 0.1.0 → 0.1.1
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/README.md +5 -0
- data/lib/omq/ffi/engine.rb +107 -3
- data/lib/omq/ffi/libzmq.rb +6 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9b5a378abbdf658759dccc080ebec5318858fe6a20cc770014d1e18b6e5fe3ff
|
|
4
|
+
data.tar.gz: ee71fd919f1315a3590b0eb464063e0056d6b3644cfe4ec98207eb2e74aebacd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bf6657d8e08d5f7820ca88b341baa8543f96be2d3454c8298913c3c9f056ef907c4b80b9cd73bd67dd1a62e1c9ddfcc09e14cd3d56d110a5f70642ac208c48ae
|
|
7
|
+
data.tar.gz: d74ee202abb7788a0b6f6a65151a884095bf9dd7cdc3acd46e2d438999f40a669ddd5bb66e20981975b5ae84bf0b2d97855ae1122180ff82c44bcd4fd2fa7dc2
|
data/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# OMQ::FFI -- libzmq Backend for OMQ
|
|
2
2
|
|
|
3
|
+
[](https://github.com/paddor/omq-ffi/actions/workflows/ci.yml)
|
|
4
|
+
[](https://rubygems.org/gems/omq-ffi)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://www.ruby-lang.org)
|
|
7
|
+
|
|
3
8
|
Use libzmq under the same OMQ socket API. Requires libzmq 4.x installed
|
|
4
9
|
on the system.
|
|
5
10
|
|
data/lib/omq/ffi/engine.rb
CHANGED
|
@@ -13,38 +13,75 @@ module OMQ
|
|
|
13
13
|
class Engine
|
|
14
14
|
L = Libzmq
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
|
|
17
|
+
# @return [Options] socket options
|
|
18
|
+
attr_reader :options
|
|
19
|
+
# @return [String, nil] the last endpoint bound or connected
|
|
20
|
+
attr_reader :last_endpoint
|
|
21
|
+
# @return [Integer, nil] TCP port of the last endpoint, if applicable
|
|
22
|
+
attr_reader :last_tcp_port
|
|
23
|
+
# @return [Array] active connections
|
|
24
|
+
attr_reader :connections
|
|
25
|
+
# @return [RoutingStub] subscription/group routing interface
|
|
26
|
+
attr_reader :routing
|
|
27
|
+
# @return [Async::Promise] resolved when the first peer connects
|
|
28
|
+
attr_reader :peer_connected
|
|
29
|
+
# @return [Async::Promise] resolved when all peers have disconnected
|
|
30
|
+
attr_reader :all_peers_gone
|
|
31
|
+
# @param value [Boolean] enables or disables automatic reconnection
|
|
18
32
|
attr_writer :reconnect_enabled
|
|
19
33
|
|
|
20
34
|
# Routing stub that delegates subscribe/unsubscribe/join/leave to
|
|
21
35
|
# libzmq socket options via the I/O thread.
|
|
22
36
|
#
|
|
23
37
|
class RoutingStub
|
|
38
|
+
# @return [Async::Promise] resolved when a subscriber joins
|
|
24
39
|
attr_reader :subscriber_joined
|
|
25
40
|
|
|
41
|
+
# @param engine [Engine] the parent engine instance
|
|
26
42
|
def initialize(engine)
|
|
27
43
|
@engine = engine
|
|
28
44
|
@subscriber_joined = Async::Promise.new
|
|
29
45
|
end
|
|
30
46
|
|
|
47
|
+
|
|
48
|
+
# Subscribes to messages matching the given prefix.
|
|
49
|
+
#
|
|
50
|
+
# @param prefix [String] subscription prefix
|
|
51
|
+
# @return [void]
|
|
31
52
|
def subscribe(prefix)
|
|
32
53
|
@engine.send_cmd(:subscribe, prefix.b)
|
|
33
54
|
end
|
|
34
55
|
|
|
56
|
+
|
|
57
|
+
# Removes a subscription for the given prefix.
|
|
58
|
+
#
|
|
59
|
+
# @param prefix [String] subscription prefix to remove
|
|
60
|
+
# @return [void]
|
|
35
61
|
def unsubscribe(prefix)
|
|
36
62
|
@engine.send_cmd(:unsubscribe, prefix.b)
|
|
37
63
|
end
|
|
38
64
|
|
|
65
|
+
|
|
66
|
+
# Joins a DISH group for receiving RADIO messages.
|
|
67
|
+
#
|
|
68
|
+
# @param group [String] group name
|
|
69
|
+
# @return [void]
|
|
39
70
|
def join(group)
|
|
40
71
|
@engine.send_cmd(:join, group)
|
|
41
72
|
end
|
|
42
73
|
|
|
74
|
+
|
|
75
|
+
# Leaves a DISH group.
|
|
76
|
+
#
|
|
77
|
+
# @param group [String] group name
|
|
78
|
+
# @return [void]
|
|
43
79
|
def leave(group)
|
|
44
80
|
@engine.send_cmd(:leave, group)
|
|
45
81
|
end
|
|
46
82
|
end
|
|
47
83
|
|
|
84
|
+
|
|
48
85
|
# @param socket_type [Symbol] e.g. :REQ, :PAIR
|
|
49
86
|
# @param options [Options]
|
|
50
87
|
#
|
|
@@ -80,8 +117,13 @@ module OMQ
|
|
|
80
117
|
@io_thread = nil
|
|
81
118
|
end
|
|
82
119
|
|
|
120
|
+
|
|
83
121
|
# --- Socket lifecycle ---
|
|
84
122
|
|
|
123
|
+
# Binds the socket to the given endpoint.
|
|
124
|
+
#
|
|
125
|
+
# @param endpoint [String] ZMQ endpoint URL (e.g. "tcp://*:5555")
|
|
126
|
+
# @return [void]
|
|
85
127
|
def bind(endpoint)
|
|
86
128
|
sync_identity
|
|
87
129
|
send_cmd(:bind, endpoint)
|
|
@@ -91,6 +133,11 @@ module OMQ
|
|
|
91
133
|
@peer_connected.resolve(:ffi) unless @peer_connected.resolved?
|
|
92
134
|
end
|
|
93
135
|
|
|
136
|
+
|
|
137
|
+
# Connects the socket to the given endpoint.
|
|
138
|
+
#
|
|
139
|
+
# @param endpoint [String] ZMQ endpoint URL
|
|
140
|
+
# @return [void]
|
|
94
141
|
def connect(endpoint)
|
|
95
142
|
sync_identity
|
|
96
143
|
send_cmd(:connect, endpoint)
|
|
@@ -100,14 +147,28 @@ module OMQ
|
|
|
100
147
|
@peer_connected.resolve(:ffi) unless @peer_connected.resolved?
|
|
101
148
|
end
|
|
102
149
|
|
|
150
|
+
|
|
151
|
+
# Disconnects from the given endpoint.
|
|
152
|
+
#
|
|
153
|
+
# @param endpoint [String] ZMQ endpoint URL
|
|
154
|
+
# @return [void]
|
|
103
155
|
def disconnect(endpoint)
|
|
104
156
|
send_cmd(:disconnect, endpoint)
|
|
105
157
|
end
|
|
106
158
|
|
|
159
|
+
|
|
160
|
+
# Unbinds from the given endpoint.
|
|
161
|
+
#
|
|
162
|
+
# @param endpoint [String] ZMQ endpoint URL
|
|
163
|
+
# @return [void]
|
|
107
164
|
def unbind(endpoint)
|
|
108
165
|
send_cmd(:unbind, endpoint)
|
|
109
166
|
end
|
|
110
167
|
|
|
168
|
+
|
|
169
|
+
# Closes the socket and shuts down the I/O thread.
|
|
170
|
+
#
|
|
171
|
+
# @return [void]
|
|
111
172
|
def close
|
|
112
173
|
return if @closed
|
|
113
174
|
@closed = true
|
|
@@ -125,6 +186,10 @@ module OMQ
|
|
|
125
186
|
@wake_w&.close rescue nil
|
|
126
187
|
end
|
|
127
188
|
|
|
189
|
+
|
|
190
|
+
# Captures the current Async task as the parent for I/O scheduling.
|
|
191
|
+
#
|
|
192
|
+
# @return [void]
|
|
128
193
|
def capture_parent_task
|
|
129
194
|
return if @parent_task
|
|
130
195
|
if Async::Task.current?
|
|
@@ -136,16 +201,26 @@ module OMQ
|
|
|
136
201
|
end
|
|
137
202
|
end
|
|
138
203
|
|
|
204
|
+
|
|
139
205
|
# --- Send ---
|
|
140
206
|
|
|
207
|
+
# Enqueues a multipart message for sending via the I/O thread.
|
|
208
|
+
#
|
|
209
|
+
# @param parts [Array<String>] message frames
|
|
210
|
+
# @return [void]
|
|
141
211
|
def enqueue_send(parts)
|
|
142
212
|
ensure_io_thread
|
|
143
213
|
@send_queue.push(parts)
|
|
144
214
|
wake_io_thread
|
|
145
215
|
end
|
|
146
216
|
|
|
217
|
+
|
|
147
218
|
# --- Recv ---
|
|
148
219
|
|
|
220
|
+
# Dequeues up to +max+ received messages, blocking until at least one is available.
|
|
221
|
+
#
|
|
222
|
+
# @param max [Integer] maximum number of messages to dequeue
|
|
223
|
+
# @return [Array<Array<String>>] batch of received multipart messages
|
|
149
224
|
def dequeue_recv_batch(max)
|
|
150
225
|
ensure_io_thread
|
|
151
226
|
msg = wait_for_message
|
|
@@ -160,11 +235,16 @@ module OMQ
|
|
|
160
235
|
batch
|
|
161
236
|
end
|
|
162
237
|
|
|
238
|
+
|
|
239
|
+
# Pushes a nil sentinel into the recv queue to unblock a waiting consumer.
|
|
240
|
+
#
|
|
241
|
+
# @return [void]
|
|
163
242
|
def dequeue_recv_sentinel
|
|
164
243
|
@recv_queue.push(nil)
|
|
165
244
|
@recv_signal_w.write_nonblock(".", exception: false) rescue nil
|
|
166
245
|
end
|
|
167
246
|
|
|
247
|
+
|
|
168
248
|
# Send a control command to the I/O thread.
|
|
169
249
|
# @api private
|
|
170
250
|
#
|
|
@@ -178,6 +258,10 @@ module OMQ
|
|
|
178
258
|
r
|
|
179
259
|
end
|
|
180
260
|
|
|
261
|
+
|
|
262
|
+
# Wakes the I/O thread via the internal pipe.
|
|
263
|
+
#
|
|
264
|
+
# @return [void]
|
|
181
265
|
def wake_io_thread
|
|
182
266
|
@wake_w.write_nonblock(".", exception: false)
|
|
183
267
|
end
|
|
@@ -199,11 +283,13 @@ module OMQ
|
|
|
199
283
|
end
|
|
200
284
|
end
|
|
201
285
|
|
|
286
|
+
|
|
202
287
|
def ensure_io_thread
|
|
203
288
|
return if @io_thread
|
|
204
289
|
@io_thread = Thread.new { io_loop }
|
|
205
290
|
end
|
|
206
291
|
|
|
292
|
+
|
|
207
293
|
# The I/O loop runs on a dedicated thread. It owns the zmq_socket
|
|
208
294
|
# exclusively and processes commands, sends, and recvs.
|
|
209
295
|
#
|
|
@@ -226,6 +312,7 @@ module OMQ
|
|
|
226
312
|
L.zmq_close(@zmq_socket)
|
|
227
313
|
end
|
|
228
314
|
|
|
315
|
+
|
|
229
316
|
def zmq_has_events?
|
|
230
317
|
@events_buf ||= ::FFI::MemoryPointer.new(:int)
|
|
231
318
|
@events_len ||= ::FFI::MemoryPointer.new(:size_t).tap { |p| p.write(:size_t, ::FFI.type_size(:int)) }
|
|
@@ -233,6 +320,7 @@ module OMQ
|
|
|
233
320
|
@events_buf.read_int != 0
|
|
234
321
|
end
|
|
235
322
|
|
|
323
|
+
|
|
236
324
|
def drain_cmds
|
|
237
325
|
loop do
|
|
238
326
|
begin
|
|
@@ -244,6 +332,7 @@ module OMQ
|
|
|
244
332
|
end
|
|
245
333
|
end
|
|
246
334
|
|
|
335
|
+
|
|
247
336
|
def process_cmd(cmd)
|
|
248
337
|
name, args, result = cmd
|
|
249
338
|
case name
|
|
@@ -284,6 +373,7 @@ module OMQ
|
|
|
284
373
|
true
|
|
285
374
|
end
|
|
286
375
|
|
|
376
|
+
|
|
287
377
|
def try_recv
|
|
288
378
|
loop do
|
|
289
379
|
parts = recv_multipart_nonblock
|
|
@@ -293,6 +383,7 @@ module OMQ
|
|
|
293
383
|
end
|
|
294
384
|
end
|
|
295
385
|
|
|
386
|
+
|
|
296
387
|
def drain_sends
|
|
297
388
|
@pending_send ||= nil
|
|
298
389
|
loop do
|
|
@@ -310,6 +401,7 @@ module OMQ
|
|
|
310
401
|
end
|
|
311
402
|
end
|
|
312
403
|
|
|
404
|
+
|
|
313
405
|
# Returns true if fully sent, false if would block (HWM).
|
|
314
406
|
#
|
|
315
407
|
def send_multipart_nonblock(parts)
|
|
@@ -328,6 +420,7 @@ module OMQ
|
|
|
328
420
|
true
|
|
329
421
|
end
|
|
330
422
|
|
|
423
|
+
|
|
331
424
|
def recv_multipart_nonblock
|
|
332
425
|
parts = []
|
|
333
426
|
loop do
|
|
@@ -349,6 +442,7 @@ module OMQ
|
|
|
349
442
|
parts.empty? ? nil : parts
|
|
350
443
|
end
|
|
351
444
|
|
|
445
|
+
|
|
352
446
|
def rcvmore?
|
|
353
447
|
buf = ::FFI::MemoryPointer.new(:int)
|
|
354
448
|
len = ::FFI::MemoryPointer.new(:size_t)
|
|
@@ -357,6 +451,7 @@ module OMQ
|
|
|
357
451
|
buf.read_int != 0
|
|
358
452
|
end
|
|
359
453
|
|
|
454
|
+
|
|
360
455
|
def get_zmq_fd
|
|
361
456
|
buf = ::FFI::MemoryPointer.new(:int)
|
|
362
457
|
len = ::FFI::MemoryPointer.new(:size_t)
|
|
@@ -365,6 +460,7 @@ module OMQ
|
|
|
365
460
|
buf.read_int
|
|
366
461
|
end
|
|
367
462
|
|
|
463
|
+
|
|
368
464
|
# Re-syncs identity to libzmq (user may set it after construction).
|
|
369
465
|
#
|
|
370
466
|
def sync_identity
|
|
@@ -374,6 +470,7 @@ module OMQ
|
|
|
374
470
|
end
|
|
375
471
|
end
|
|
376
472
|
|
|
473
|
+
|
|
377
474
|
def apply_options
|
|
378
475
|
set_int_option(L::ZMQ_SNDHWM, @options.send_hwm)
|
|
379
476
|
set_int_option(L::ZMQ_RCVHWM, @options.recv_hwm)
|
|
@@ -401,23 +498,27 @@ module OMQ
|
|
|
401
498
|
set_int_option(L::ZMQ_ROUTER_MANDATORY, 1) if @options.router_mandatory
|
|
402
499
|
end
|
|
403
500
|
|
|
501
|
+
|
|
404
502
|
def set_int_option(opt, value)
|
|
405
503
|
buf = ::FFI::MemoryPointer.new(:int)
|
|
406
504
|
buf.write_int(value)
|
|
407
505
|
L.zmq_setsockopt(@zmq_socket, opt, buf, ::FFI.type_size(:int))
|
|
408
506
|
end
|
|
409
507
|
|
|
508
|
+
|
|
410
509
|
def set_int64_option(opt, value)
|
|
411
510
|
buf = ::FFI::MemoryPointer.new(:int64)
|
|
412
511
|
buf.write_int64(value)
|
|
413
512
|
L.zmq_setsockopt(@zmq_socket, opt, buf, ::FFI.type_size(:int64))
|
|
414
513
|
end
|
|
415
514
|
|
|
515
|
+
|
|
416
516
|
def set_bytes_option(opt, value)
|
|
417
517
|
buf = ::FFI::MemoryPointer.from_string(value)
|
|
418
518
|
L.zmq_setsockopt(@zmq_socket, opt, buf, value.bytesize)
|
|
419
519
|
end
|
|
420
520
|
|
|
521
|
+
|
|
421
522
|
def get_string_option(opt)
|
|
422
523
|
buf = ::FFI::MemoryPointer.new(:char, 256)
|
|
423
524
|
len = ::FFI::MemoryPointer.new(:size_t)
|
|
@@ -426,6 +527,7 @@ module OMQ
|
|
|
426
527
|
buf.read_string(len.read(:size_t) - 1)
|
|
427
528
|
end
|
|
428
529
|
|
|
530
|
+
|
|
429
531
|
def extract_tcp_port(endpoint)
|
|
430
532
|
return nil unless endpoint
|
|
431
533
|
port = endpoint.split(":").last.to_i
|
|
@@ -433,8 +535,10 @@ module OMQ
|
|
|
433
535
|
end
|
|
434
536
|
end
|
|
435
537
|
|
|
436
|
-
|
|
538
|
+
|
|
539
|
+
# Returns the shared ZMQ context (one per process, lazily initialized).
|
|
437
540
|
#
|
|
541
|
+
# @return [FFI::Pointer] zmq context pointer
|
|
438
542
|
def self.context
|
|
439
543
|
@context ||= Libzmq.zmq_ctx_new.tap do |ctx|
|
|
440
544
|
raise "zmq_ctx_new failed" if ctx.null?
|
data/lib/omq/ffi/libzmq.rb
CHANGED
|
@@ -44,6 +44,7 @@ module OMQ
|
|
|
44
44
|
# libzmq built without ZMQ_BUILD_DRAFT_API
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
+
|
|
47
48
|
# Error
|
|
48
49
|
attach_function :zmq_errno, [], :int
|
|
49
50
|
attach_function :zmq_strerror, [:int], :string
|
|
@@ -69,6 +70,7 @@ module OMQ
|
|
|
69
70
|
ZMQ_PEER = 19
|
|
70
71
|
ZMQ_CHANNEL = 20
|
|
71
72
|
|
|
73
|
+
|
|
72
74
|
# Socket type name → constant
|
|
73
75
|
SOCKET_TYPES = {
|
|
74
76
|
PAIR: ZMQ_PAIR, PUB: ZMQ_PUB, SUB: ZMQ_SUB,
|
|
@@ -86,6 +88,7 @@ module OMQ
|
|
|
86
88
|
ZMQ_DONTWAIT = 1
|
|
87
89
|
ZMQ_SNDMORE = 2
|
|
88
90
|
|
|
91
|
+
|
|
89
92
|
# Socket options
|
|
90
93
|
ZMQ_IDENTITY = 5
|
|
91
94
|
ZMQ_SUBSCRIBE = 6
|
|
@@ -105,9 +108,11 @@ module OMQ
|
|
|
105
108
|
ZMQ_RECONNECT_IVL_MAX = 21
|
|
106
109
|
ZMQ_CONFLATE = 54
|
|
107
110
|
|
|
111
|
+
|
|
108
112
|
# zmq_msg_t is 64 bytes on all platforms
|
|
109
113
|
MSG_T_SIZE = 64
|
|
110
114
|
|
|
115
|
+
|
|
111
116
|
# Allocates a zmq_msg_t on the heap.
|
|
112
117
|
#
|
|
113
118
|
# @return [FFI::MemoryPointer]
|
|
@@ -116,6 +121,7 @@ module OMQ
|
|
|
116
121
|
::FFI::MemoryPointer.new(MSG_T_SIZE)
|
|
117
122
|
end
|
|
118
123
|
|
|
124
|
+
|
|
119
125
|
# Raises an error with the current zmq_errno message.
|
|
120
126
|
#
|
|
121
127
|
def self.check!(rc, label = "zmq")
|