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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 88e0942ce849494a8af0baa0cc5d894a27c6c64c7ea2e15a726a203df262296d
4
- data.tar.gz: 4d9e966c9bbdb5317112dd1aed5f7037f4b911f3096cfe3e103f75dbd019588d
3
+ metadata.gz: 9b5a378abbdf658759dccc080ebec5318858fe6a20cc770014d1e18b6e5fe3ff
4
+ data.tar.gz: ee71fd919f1315a3590b0eb464063e0056d6b3644cfe4ec98207eb2e74aebacd
5
5
  SHA512:
6
- metadata.gz: 83f0e5f7e637634e5d07a56d91f4e3887b4d542b3cad88fc2189755b3857b01693871a10f0a5a2c4939981b634acd3a7915e8cd247947694fdb9c3afe4ce74d6
7
- data.tar.gz: 4f1e5da9f453d8fa90816b16c712e718e1ad7855be0dfa53fa18582cbb52c217057034e2dbe1c432b073c12a0d50debc21989321817e4462bec927637c2fdfeb
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
+ [![CI](https://github.com/paddor/omq-ffi/actions/workflows/ci.yml/badge.svg)](https://github.com/paddor/omq-ffi/actions/workflows/ci.yml)
4
+ [![Gem Version](https://img.shields.io/gem/v/omq-ffi?color=e9573f)](https://rubygems.org/gems/omq-ffi)
5
+ [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](LICENSE)
6
+ [![Ruby](https://img.shields.io/badge/Ruby-%3E%3D%203.3-CC342D?logo=ruby&logoColor=white)](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
 
@@ -13,38 +13,75 @@ module OMQ
13
13
  class Engine
14
14
  L = Libzmq
15
15
 
16
- attr_reader :options, :last_endpoint, :last_tcp_port, :connections, :routing
17
- attr_reader :peer_connected, :all_peers_gone
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
- # Shared ZMQ context — one per process.
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?
@@ -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")
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omq-ffi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrik Wenger