cztop 2.0.2 → 2.0.4

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: 8c9e575b8a9eb164ac1dfa55ed1cf586f9f37238f1c8b2e46a9063ef897ff3b5
4
- data.tar.gz: 829ed1baf3e3c0ec4816ff021c866bd40aaa8134f8332b8f9f56635d27d22730
3
+ metadata.gz: e3c85c43224c3d6b657fdc1178c3dc145bae175ab4febd29194717074509c3e0
4
+ data.tar.gz: 70b7dd7ed91196fa9c24bd911be6d30f5c5f9c3b5a55f70c8a78f4cfbbb8d74c
5
5
  SHA512:
6
- metadata.gz: 3f0fb0eb3c60d9d792d6122cf61c60b7e705f6252d6bc760363a1490a7354f628dd3d3f982858bea435ebc928c60752197727f6f522fe894b77bdbb755773f98
7
- data.tar.gz: 9fa64c0c8e5896146e5cd2b2034ffd6e663416f4c6bcfe8ffcf4260f683524fc83a773f8da31b2284fb858284ca18c2897a8114c11b26d893150ffda775a40d8
6
+ metadata.gz: 34a251ab8bd12c5220dea4627f7cf3823d0aaf590da6875ba182d92107defd17a68100e638d8feca6b348608585d80f0002af77dfb8f34c68cc7d00fd5af2cb0
7
+ data.tar.gz: 70ee8e3f715ab38d820e48919e0c7e8608cdcd836383ed969ec712447b14db74b7706cc9b7e86d40f9c1b71ec2d2300a3287cdbc39edbb6270e1e00576e6d9b5
data/CHANGES.md CHANGED
@@ -1,3 +1,32 @@
1
+ ## [Unreleased]
2
+
3
+ 2.0.4
4
+ -----
5
+
6
+ * Prevent socket double destruction by removing finalizers before native cleanup
7
+ and sharing their pointer holders with explicit destruction
8
+ * Prevent monitor double destruction with the same finalizer cleanup ordering
9
+ and shared pointer holder
10
+ * Keep parent messages alive while borrowed frames remain live, and reject frame
11
+ access after message ownership is transferred
12
+
13
+ 2.0.3
14
+ -----
15
+
16
+ * Port specs to named `Minitest::Spec` subclasses with ordinary Ruby helpers
17
+ * Add regression coverage for native library loading and fallback
18
+ * Raise native send/receive errors immediately instead of waiting for readiness
19
+ * Raise `IO::EAGAINWaitReadable`/`IO::EAGAINWaitWritable` when nonblocking I/O
20
+ would block
21
+ * Convert all multipart message parts before sending the first frame
22
+ * Avoid an extra array allocation for scalar sends without modifying caller arrays
23
+ * Free native identity strings and messages retained after failed sends
24
+ * Reject endpoint and subscription operations on closed sockets, and receive
25
+ operations on closed monitors
26
+ * Close the CURVE authentication socket in its responder thread during shutdown
27
+ * Fix linger documentation to use seconds
28
+ * Clean up benchmark socket lifetimes and refresh measurements and README
29
+
1
30
  2.0.2
2
31
  -----
3
32
 
data/README.md CHANGED
@@ -5,17 +5,17 @@
5
5
  [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](LICENSE)
6
6
  [![Ruby](https://img.shields.io/badge/Ruby-%3E%3D%203.3-CC342D?logo=ruby&logoColor=white)](https://www.ruby-lang.org)
7
7
 
8
- Ruby FFI binding for [CZMQ](http://czmq.zeromq.org/) / [ZeroMQ](https://zeromq.org/) — high-performance asynchronous messaging for distributed systems.
8
+ Ruby FFI binding for [CZMQ](https://github.com/zeromq/czmq) and [ZeroMQ](https://zeromq.org/), with support for threads, Fiber Scheduler, and CURVE encryption.
9
9
 
10
- > **353k msg/s** inproc throughput | **49 µs** fiber roundtrip latency | nonblock fast path
10
+ > **310k msg/s** inproc throughput (64 B) | **61 µs** Async roundtrip latency
11
11
 
12
12
  ---
13
13
 
14
14
  ## Highlights
15
15
 
16
- - **All socket types** — req/rep, pub/sub, push/pull, dealer/router, xpub/xsub, pair, stream
17
- - **Async-first** — first-class [async](https://github.com/socketry/async) fiber support, also works with plain threads
18
- - **Ruby-idiomatic API** — messages as `Array<String>`, errors as exceptions, timeouts as `IO::TimeoutError`
16
+ - **Socket types:** REQ/REP, PUB/SUB, PUSH/PULL, DEALER/ROUTER, XPUB/XSUB, PAIR, STREAM
17
+ - **Fibers and threads:** works with [async](https://github.com/socketry/async) and plain threads
18
+ - **Ruby API:** messages as `Array<String>`, errors as exceptions, timeouts as `IO::TimeoutError`
19
19
 
20
20
  ## Install
21
21
 
@@ -33,17 +33,21 @@ Then add the gem:
33
33
 
34
34
  ```sh
35
35
  gem install cztop
36
- # or in Gemfile
36
+ ```
37
+
38
+ Or add it to your Gemfile:
39
+
40
+ ```ruby
37
41
  gem 'cztop'
38
42
  ```
39
43
 
40
44
  ## Learning ZeroMQ
41
45
 
42
- New to ZeroMQ? See [ZGUIDE_SUMMARY.md](ZGUIDE_SUMMARY.md) — a ~30 min read covering all major patterns with working CZTop code examples.
46
+ New to ZeroMQ? See [ZGUIDE_SUMMARY.md](ZGUIDE_SUMMARY.md) for messaging patterns and working CZTop examples.
43
47
 
44
48
  ## Quick Start
45
49
 
46
- ### Request / Reply
50
+ ### REQ / REP
47
51
 
48
52
  ```ruby
49
53
  require 'cztop'
@@ -63,7 +67,7 @@ Async do |task|
63
67
  end
64
68
  ```
65
69
 
66
- ### Pub / Sub
70
+ ### PUB / SUB
67
71
 
68
72
  ```ruby
69
73
  Async do |task|
@@ -78,7 +82,7 @@ Async do |task|
78
82
  end
79
83
  ```
80
84
 
81
- ### Push / Pull (Pipeline)
85
+ ### PUSH / PULL (Pipeline)
82
86
 
83
87
  ```ruby
84
88
  Async do
@@ -95,7 +99,8 @@ end
95
99
  | Pattern | Classes | Direction |
96
100
  |---------|---------|-----------|
97
101
  | Request/Reply | `REQ`, `REP` | bidirectional |
98
- | Publish/Subscribe | `PUB`, `SUB`, `XPUB`, `XSUB` | unidirectional |
102
+ | Publish/Subscribe | `PUB`, `SUB` | unidirectional |
103
+ | Subscription forwarding | `XPUB`, `XSUB` | bidirectional |
99
104
  | Pipeline | `PUSH`, `PULL` | unidirectional |
100
105
  | Routing | `DEALER`, `ROUTER` | bidirectional |
101
106
  | Exclusive pair | `PAIR` | bidirectional |
@@ -105,23 +110,23 @@ All classes live under `CZTop::Socket::`.
105
110
 
106
111
  ## Performance
107
112
 
108
- Benchmarked with benchmark-ips on Linux x86_64 (CZMQ 4.2.1, ZMQ 4.3.5, Ruby 4.0.1 +YJIT):
113
+ Benchmarked with benchmark-ips on Linux x86_64 (CZMQ 4.2.1, ZMQ 4.3.5, Ruby 4.0.6 +YJIT):
109
114
 
110
- #### Throughput (push/pull)
115
+ #### Throughput (PUSH/PULL, 64 B messages)
111
116
 
112
117
  | | inproc | ipc | tcp |
113
118
  |---|--------|-----|-----|
114
- | **Async** | 284k/s | 17k/s | 14k/s |
115
- | **Threads** | 353k/s | 25k/s | 21k/s |
119
+ | **Async** | 278k/s | 18.3k/s | 15.8k/s |
120
+ | **Threads** | 310k/s | 18.3k/s | 16.2k/s |
116
121
 
117
- #### Latency (req/rep roundtrip)
122
+ #### Latency (REQ/REP roundtrip)
118
123
 
119
124
  | | inproc | ipc | tcp |
120
125
  |---|--------|-----|-----|
121
- | **Async** | 49 µs | 100 µs | 107 µs |
122
- | **Threads** | 113 µs | 154 µs | 168 µs |
126
+ | **Async** | 60.7 µs | 112.6 µs | 127.8 µs |
127
+ | **Threads** | 74.9 µs | 109.2 µs | 119.6 µs |
123
128
 
124
- Async fibers deliver 2.3x lower inproc latency thanks to cheap context switching. See [`bench/`](bench/) for full results and scripts.
129
+ See [bench/README.md](bench/README.md) for full results, measurement details, and scripts.
125
130
 
126
131
  ## API Reference
127
132
 
@@ -29,7 +29,9 @@ module CZTop
29
29
  @allow_any = allow_any
30
30
  @zap = CZTop::Socket::REP.new
31
31
  @zap.linger = 0
32
+ @zap.recv_timeout = 0.1
32
33
  @zap.bind('inproc://zeromq.zap.01')
34
+ @stopping = false
33
35
  @thread = Thread.new { run }
34
36
  ObjectSpace.define_finalizer(self, self.class._poststop(@thread))
35
37
  end
@@ -60,8 +62,9 @@ module CZTop
60
62
  #
61
63
  def stop
62
64
  ObjectSpace.undefine_finalizer(self)
63
- @zap.close
64
- @thread.join(1)
65
+ @stopping = true
66
+ @thread.join
67
+ nil
65
68
  end
66
69
 
67
70
  # @api private
@@ -75,8 +78,12 @@ module CZTop
75
78
  private
76
79
 
77
80
  def run
78
- loop do
79
- msg = @zap.receive
81
+ until @stopping
82
+ begin
83
+ msg = @zap.receive
84
+ rescue IO::TimeoutError
85
+ next
86
+ end
80
87
  # ZAP request: [version, request_id, domain, address, identity, mechanism, credential]
81
88
  version = msg[0]
82
89
  request_id = msg[1]
@@ -90,8 +97,8 @@ module CZTop
90
97
 
91
98
  @zap.__send__(:send, [version, request_id, status, '', '', ''])
92
99
  end
93
- rescue
94
- # Socket closed or thread interrupted → exit gracefully
100
+ ensure
101
+ @zap.close
95
102
  end
96
103
 
97
104
  end
data/lib/cztop/curve.rb CHANGED
@@ -6,7 +6,7 @@ module CZTop
6
6
  #
7
7
  # Provides keypair generation, key derivation, and Z85 encoding/decoding.
8
8
  # CURVE options are applied to sockets via the +curve:+ keyword argument
9
- # in socket constructors — there are no public CURVE methods on Socket.
9
+ # in socket constructors; there are no public CURVE methods on Socket.
10
10
  #
11
11
  module CURVE
12
12
 
data/lib/cztop/ffi.rb CHANGED
@@ -6,6 +6,8 @@ module CZMQ
6
6
  module FFI
7
7
  extend ::FFI::Library
8
8
 
9
+ # Runtime packages may only include versioned sonames. Keep each library's
10
+ # alternatives in one array so FFI can fall back to platform-specific names.
9
11
  ffi_lib ['libzmq.so.5', 'libzmq'], ['libczmq.so.4', 'libczmq']
10
12
 
11
13
  # -----------------------------------------------------------------
@@ -122,6 +124,7 @@ module CZMQ
122
124
  # -----------------------------------------------------------------
123
125
  attach_function :zstr_send, [:pointer, :string], :int, **opts
124
126
  attach_function :zstr_sendm, [:pointer, :string], :int, **opts
127
+ attach_function :zstr_free, [:pointer], :void
125
128
 
126
129
  # -----------------------------------------------------------------
127
130
  # zsock_wait (used for zactor START synchronization)
@@ -169,8 +172,8 @@ module CZMQ
169
172
  end
170
173
 
171
174
  module ClassMethods
172
- def prevent_leak(ptr, destroy_fn)
173
- prevent_leak_ptr = ::FFI::MemoryPointer.new(:pointer)
175
+ def prevent_leak(ptr, destroy_fn, ptr_ref: nil)
176
+ prevent_leak_ptr = ptr_ref || ::FFI::MemoryPointer.new(:pointer)
174
177
  prevent_leak_ptr.write_pointer(ptr)
175
178
  prevent_leak_fn = destroy_fn
176
179
  ->(id) { CZMQ::FFI.__send__(prevent_leak_fn, prevent_leak_ptr) }
@@ -261,10 +264,11 @@ module CZMQ
261
264
 
262
265
  def initialize(type)
263
266
  @ptr = CZMQ::FFI.zsock_new(type)
267
+ @ptr_ref = ::FFI::MemoryPointer.new(:pointer)
264
268
  @moved = false
265
269
  unless @ptr.null?
266
270
  ObjectSpace.define_finalizer(self,
267
- self.class.prevent_leak(@ptr, :zsock_destroy))
271
+ self.class.prevent_leak(@ptr, :zsock_destroy, ptr_ref: @ptr_ref))
268
272
  end
269
273
  end
270
274
 
@@ -284,11 +288,13 @@ module CZMQ
284
288
 
285
289
  def self._wrap(ptr)
286
290
  obj = allocate
291
+ ptr_ref = ::FFI::MemoryPointer.new(:pointer)
287
292
  obj.instance_variable_set(:@ptr, ptr)
293
+ obj.instance_variable_set(:@ptr_ref, ptr_ref)
288
294
  obj.instance_variable_set(:@moved, false)
289
295
  unless ptr.null?
290
296
  ObjectSpace.define_finalizer(obj,
291
- prevent_leak(ptr, :zsock_destroy))
297
+ prevent_leak(ptr, :zsock_destroy, ptr_ref: ptr_ref))
292
298
  end
293
299
  obj
294
300
  end
@@ -296,40 +302,39 @@ module CZMQ
296
302
 
297
303
  # Instance methods
298
304
  def endpoint
299
- CZMQ::FFI.zsock_endpoint(@ptr)
305
+ CZMQ::FFI.zsock_endpoint(to_ptr)
300
306
  end
301
307
 
302
308
  def connect(format, *args)
303
- CZMQ::FFI.zsock_connect(@ptr, format, *args)
309
+ CZMQ::FFI.zsock_connect(to_ptr, format, *args)
304
310
  end
305
311
 
306
312
  def disconnect(format, *args)
307
- CZMQ::FFI.zsock_disconnect(@ptr, format, *args)
313
+ CZMQ::FFI.zsock_disconnect(to_ptr, format, *args)
308
314
  end
309
315
 
310
316
  def bind(format, *args)
311
- CZMQ::FFI.zsock_bind(@ptr, format, *args)
317
+ CZMQ::FFI.zsock_bind(to_ptr, format, *args)
312
318
  end
313
319
 
314
320
  def unbind(format, *args)
315
- CZMQ::FFI.zsock_unbind(@ptr, format, *args)
321
+ CZMQ::FFI.zsock_unbind(to_ptr, format, *args)
316
322
  end
317
323
 
318
324
  def destroy
319
325
  return if @moved
320
- ptr_ptr = ::FFI::MemoryPointer.new(:pointer)
321
- ptr_ptr.write_pointer(@ptr)
322
- CZMQ::FFI.zsock_destroy(ptr_ptr)
323
326
  @moved = true
324
327
  ObjectSpace.undefine_finalizer(self)
328
+ # Native destruction releases the GVL and nulls the finalizer's reference.
329
+ CZMQ::FFI.zsock_destroy(@ptr_ref)
325
330
  end
326
331
 
327
332
  def set_subscribe(prefix)
328
- CZMQ::FFI.zsock_set_subscribe(@ptr, prefix)
333
+ CZMQ::FFI.zsock_set_subscribe(to_ptr, prefix)
329
334
  end
330
335
 
331
336
  def set_unsubscribe(prefix)
332
- CZMQ::FFI.zsock_set_unsubscribe(@ptr, prefix)
337
+ CZMQ::FFI.zsock_set_unsubscribe(to_ptr, prefix)
333
338
  end
334
339
 
335
340
  # Class methods that take a zsock pointer (or wrapper with to_ptr)
@@ -423,9 +428,15 @@ module CZMQ
423
428
  # Override Object#send: int zmsg_send(zmsg_t **self_p, void *dest)
424
429
  # Called as: Zmsg.send(zmsg_wrapper_or_ptr, destination)
425
430
  def self.send(msg, dest)
426
- msg_ptr = msg.respond_to?(:__ptr_give_ref) ? msg.__ptr_give_ref : msg
427
431
  dest_ptr = dest.respond_to?(:to_ptr) ? dest.to_ptr : dest
428
- CZMQ::FFI.zmsg_send(msg_ptr, dest_ptr)
432
+ msg_ptr = msg.respond_to?(:__ptr_give_ref) ? msg.__ptr_give_ref : msg
433
+ rc = CZMQ::FFI.zmsg_send(msg_ptr, dest_ptr)
434
+ unless rc.zero?
435
+ errno = CZMQ::FFI::Errors.errno
436
+ CZMQ::FFI.zmsg_destroy(msg_ptr) if msg.respond_to?(:__ptr_give_ref)
437
+ CZTop::HasFFIDelegate.raise_zmq_err(errno: errno)
438
+ end
439
+ rc
429
440
  end
430
441
 
431
442
  def self.recv(source)
@@ -437,17 +448,17 @@ module CZMQ
437
448
  # Appends a Ruby string directly without intermediate MemoryPointer copy.
438
449
  # Uses :buffer_in to pass the string's internal buffer pointer to C.
439
450
  def add_buffer(str)
440
- CZMQ::FFI.zmsg_addmem_s(@ptr, str, str.bytesize)
451
+ CZMQ::FFI.zmsg_addmem_s(to_ptr, str, str.bytesize)
441
452
  end
442
453
 
443
454
  def first
444
- ptr = CZMQ::FFI.zmsg_first(@ptr)
455
+ ptr = CZMQ::FFI.zmsg_first(to_ptr)
445
456
  return nil if ptr.null?
446
457
  _borrowed_frame(ptr)
447
458
  end
448
459
 
449
460
  def next
450
- ptr = CZMQ::FFI.zmsg_next(@ptr)
461
+ ptr = CZMQ::FFI.zmsg_next(to_ptr)
451
462
  return nil if ptr.null?
452
463
  _borrowed_frame(ptr)
453
464
  end
@@ -456,27 +467,30 @@ module CZMQ
456
467
 
457
468
  # Returns a Zframe that does NOT own the pointer (borrowed from zmsg).
458
469
  def _borrowed_frame(ptr)
459
- Zframe._from_ptr(ptr, false)
470
+ Zframe._from_ptr(ptr, owner: self)
460
471
  end
461
472
  end
462
473
 
463
474
  # -----------------------------------------------------------------
464
- # Zframe (lightweight — only used for borrowed frames from Zmsg)
475
+ # Zframe (lightweight; only used for borrowed frames from Zmsg)
465
476
  # -----------------------------------------------------------------
466
477
  class Zframe
467
- # Wraps a borrowed zframe_t pointer (not owned, no finalizer).
478
+ # Wraps a borrowed zframe_t pointer and retains its owning message.
468
479
  #
469
- def self._from_ptr(ptr, _owned = false)
480
+ def self._from_ptr(ptr, owner:)
470
481
  obj = allocate
471
482
  obj.instance_variable_set(:@ptr, ptr)
483
+ obj.instance_variable_set(:@owner, owner)
472
484
  obj
473
485
  end
474
486
 
475
487
  def data
488
+ @owner.to_ptr
476
489
  CZMQ::FFI.zframe_data(@ptr)
477
490
  end
478
491
 
479
492
  def size
493
+ @owner.to_ptr
480
494
  CZMQ::FFI.zframe_size(@ptr)
481
495
  end
482
496
  end
data/lib/cztop/monitor.rb CHANGED
@@ -46,9 +46,9 @@ module CZTop
46
46
 
47
47
  @closed = false
48
48
 
49
- prevent_leak_ptr = ::FFI::MemoryPointer.new(:pointer)
50
- prevent_leak_ptr.write_pointer(@actor_ptr)
51
- ObjectSpace.define_finalizer(self, self.class._make_destructor(prevent_leak_ptr))
49
+ @actor_ref = ::FFI::MemoryPointer.new(:pointer)
50
+ @actor_ref.write_pointer(@actor_ptr)
51
+ ObjectSpace.define_finalizer(self, self.class._make_destructor(@actor_ref))
52
52
 
53
53
  CZMQ::FFI.zstr_send(@actor_ptr, 'VERBOSE') if verbose
54
54
 
@@ -69,6 +69,7 @@ module CZTop
69
69
  # @return [Event, nil] the event, or nil on timeout
70
70
  #
71
71
  def receive(timeout: 0.1)
72
+ raise IOError, 'monitor is closed' if @closed
72
73
  CZMQ::FFI.zsock_set_rcvtimeo(@actor_ptr, (timeout * 1000).to_i)
73
74
 
74
75
  zmsg_ptr = CZMQ::FFI.zmsg_recv(@actor_ptr)
@@ -102,11 +103,9 @@ module CZTop
102
103
  def close
103
104
  return if @closed
104
105
 
105
- pp = ::FFI::MemoryPointer.new(:pointer)
106
- pp.write_pointer(@actor_ptr)
107
- CZMQ::FFI.zactor_destroy(pp)
108
106
  @closed = true
109
107
  ObjectSpace.undefine_finalizer(self)
108
+ CZMQ::FFI.zactor_destroy(@actor_ref)
110
109
  end
111
110
 
112
111
 
@@ -13,7 +13,7 @@ module CZTop
13
13
 
14
14
  # @param endpoints [String] endpoints to connect to
15
15
  # @param curve [Hash, nil] CURVE encryption options
16
- # @param linger [Integer] linger period in milliseconds (default: 0)
16
+ # @param linger [Numeric, nil] linger period in seconds (default: 0)
17
17
  #
18
18
  def initialize(endpoints = nil, curve: nil, linger: 0)
19
19
  super
@@ -13,7 +13,7 @@ module CZTop
13
13
 
14
14
  # @param endpoints [String] endpoints to connect to
15
15
  # @param curve [Hash, nil] CURVE encryption options
16
- # @param linger [Integer] linger period in milliseconds (default: 0)
16
+ # @param linger [Numeric, nil] linger period in seconds (default: 0)
17
17
  #
18
18
  def initialize(endpoints = nil, curve: nil, linger: 0)
19
19
  super
@@ -12,7 +12,7 @@ module CZTop
12
12
 
13
13
  # @param endpoints [String] endpoints to bind to
14
14
  # @param curve [Hash, nil] CURVE encryption options
15
- # @param linger [Integer] linger period in milliseconds (default: 0)
15
+ # @param linger [Numeric, nil] linger period in seconds (default: 0)
16
16
  #
17
17
  def initialize(endpoints = nil, curve: nil, linger: 0)
18
18
  super
@@ -12,7 +12,7 @@ module CZTop
12
12
 
13
13
  # @param endpoints [String] endpoints to bind to
14
14
  # @param curve [Hash, nil] CURVE encryption options
15
- # @param linger [Integer] linger period in milliseconds (default: 0)
15
+ # @param linger [Numeric, nil] linger period in seconds (default: 0)
16
16
  #
17
17
  def initialize(endpoints = nil, curve: nil, linger: 0)
18
18
  super
@@ -12,7 +12,7 @@ module CZTop
12
12
 
13
13
  # @param endpoints [String] endpoints to connect to
14
14
  # @param curve [Hash, nil] CURVE encryption options
15
- # @param linger [Integer] linger period in milliseconds (default: 0)
15
+ # @param linger [Numeric, nil] linger period in seconds (default: 0)
16
16
  #
17
17
  def initialize(endpoints = nil, curve: nil, linger: 0)
18
18
  super
@@ -18,6 +18,7 @@ module CZTop
18
18
  # Fast path: nonblock recv (no GVL release)
19
19
  parts = recv_nonblock
20
20
  return parts if parts
21
+ raise IO::EAGAINWaitReadable if read_timeout == 0
21
22
 
22
23
  # Slow path: FD poll → blocking recv
23
24
  wait_readable
@@ -56,7 +57,11 @@ module CZTop
56
57
  def recv_nonblock
57
58
  sock_ptr = to_ptr
58
59
  frame = CZMQ::FFI.zframe_recv_nowait(sock_ptr)
59
- return nil if frame.null?
60
+ if frame.null?
61
+ errno = CZMQ::FFI::Errors.errno
62
+ HasFFIDelegate.raise_zmq_err(errno: errno) unless errno == Errno::EAGAIN::Errno
63
+ return nil
64
+ end
60
65
 
61
66
  pp = _zframe_pp
62
67
  parts = []
@@ -13,7 +13,7 @@ module CZTop
13
13
 
14
14
  # @param endpoints [String] endpoints to bind to
15
15
  # @param curve [Hash, nil] CURVE encryption options
16
- # @param linger [Integer] linger period in milliseconds (default: 0)
16
+ # @param linger [Numeric, nil] linger period in seconds (default: 0)
17
17
  #
18
18
  def initialize(endpoints = nil, curve: nil, linger: 0)
19
19
  super
@@ -13,7 +13,7 @@ module CZTop
13
13
 
14
14
  # @param endpoints [String] endpoints to connect to
15
15
  # @param curve [Hash, nil] CURVE encryption options
16
- # @param linger [Integer] linger period in milliseconds (default: 0)
16
+ # @param linger [Numeric, nil] linger period in seconds (default: 0)
17
17
  #
18
18
  def initialize(endpoints = nil, curve: nil, linger: 0)
19
19
  super
@@ -13,7 +13,7 @@ module CZTop
13
13
 
14
14
  # @param endpoints [String] endpoints to bind to
15
15
  # @param curve [Hash, nil] CURVE encryption options
16
- # @param linger [Integer] linger period in milliseconds (default: 0)
16
+ # @param linger [Numeric, nil] linger period in seconds (default: 0)
17
17
  #
18
18
  def initialize(endpoints = nil, curve: nil, linger: 0)
19
19
  super
@@ -14,7 +14,7 @@ module CZTop
14
14
 
15
15
  # @param endpoints [String] endpoints to connect to
16
16
  # @param curve [Hash, nil] CURVE encryption options
17
- # @param linger [Integer] linger period in milliseconds (default: 0)
17
+ # @param linger [Numeric, nil] linger period in seconds (default: 0)
18
18
  #
19
19
  def initialize(endpoints = nil, curve: nil, linger: 0)
20
20
  super
@@ -17,7 +17,7 @@ module CZTop
17
17
  # @param prefix [String, nil] subscription prefix; defaults to no
18
18
  # subscription. Pass {EVERYTHING} (+""+) to subscribe to everything.
19
19
  # @param curve [Hash, nil] CURVE encryption options
20
- # @param linger [Integer] linger period in milliseconds (default: 0)
20
+ # @param linger [Numeric, nil] linger period in seconds (default: 0)
21
21
  #
22
22
  def initialize(endpoints = nil, prefix: nil, curve: nil, linger: 0)
23
23
  super(endpoints, curve: curve, linger: linger)
@@ -16,18 +16,19 @@ module CZTop
16
16
  # @return [self]
17
17
  #
18
18
  def send(message)
19
- parts = message.is_a?(Array) ? message : [message]
19
+ parts = message.is_a?(Array) ? message.map(&:to_s) : [message.to_s]
20
20
  raise ArgumentError, 'message has no parts' if parts.empty?
21
21
 
22
22
  # Fast path: nonblock send (no GVL release)
23
23
  return self if send_nonblock(parts)
24
+ raise IO::EAGAINWaitWritable if write_timeout == 0
24
25
 
25
26
  # Slow path: FD poll → blocking send
26
27
  wait_writable
27
28
 
28
29
  zmsg = CZMQ::FFI::Zmsg.new
29
30
  parts.each do |part|
30
- rc = zmsg.add_buffer(part.to_s)
31
+ rc = zmsg.add_buffer(part)
31
32
  HasFFIDelegate.raise_zmq_err unless rc.zero?
32
33
  end
33
34
 
@@ -65,9 +66,8 @@ module CZTop
65
66
  last_idx = parts.size - 1
66
67
 
67
68
  parts.each_with_index do |part, i|
68
- str = part.to_s
69
- frame = CZMQ::FFI.zframe_new_s(str, str.bytesize)
70
- return false if frame.null?
69
+ frame = CZMQ::FFI.zframe_new_s(part, part.bytesize)
70
+ raise NoMemoryError, 'could not allocate message frame' if frame.null?
71
71
 
72
72
  flags = CZMQ::FFI::ZFRAME_DONTWAIT
73
73
  flags |= CZMQ::FFI::ZFRAME_MORE if i < last_idx
@@ -75,7 +75,9 @@ module CZTop
75
75
  pp.write_pointer(frame)
76
76
  rc = CZMQ::FFI.zframe_send(pp, sock_ptr, flags)
77
77
  unless rc.zero?
78
+ errno = CZMQ::FFI::Errors.errno
78
79
  CZMQ::FFI.zframe_destroy(pp)
80
+ HasFFIDelegate.raise_zmq_err(errno: errno) unless errno == Errno::EAGAIN::Errno
79
81
  return false
80
82
  end
81
83
  end
@@ -13,7 +13,7 @@ module CZTop
13
13
 
14
14
  # @param endpoints [String] endpoints to bind to
15
15
  # @param curve [Hash, nil] CURVE encryption options
16
- # @param linger [Integer] linger period in milliseconds (default: 0)
16
+ # @param linger [Numeric, nil] linger period in seconds (default: 0)
17
17
  #
18
18
  def initialize(endpoints = nil, curve: nil, linger: 0)
19
19
  super
@@ -13,7 +13,7 @@ module CZTop
13
13
 
14
14
  # @param endpoints [String] endpoints to connect to
15
15
  # @param curve [Hash, nil] CURVE encryption options
16
- # @param linger [Integer] linger period in milliseconds (default: 0)
16
+ # @param linger [Numeric, nil] linger period in seconds (default: 0)
17
17
  #
18
18
  def initialize(endpoints = nil, curve: nil, linger: 0)
19
19
  super
data/lib/cztop/socket.rb CHANGED
@@ -142,7 +142,7 @@ module CZTop
142
142
  # Connects or binds based on CZMQ endpoint prefix convention.
143
143
  # @api private
144
144
  # @param endpoints [String] endpoint, optionally prefixed with +@+ (bind) or +>+ (connect)
145
- # @param default [Symbol] +:connect+ or +:bind+ — action when no prefix given
145
+ # @param default [Symbol] +:connect+ or +:bind+; action when no prefix given
146
146
  #
147
147
  def _attach(endpoints, default:)
148
148
  return unless endpoints
data/lib/cztop/version.rb CHANGED
@@ -2,6 +2,6 @@
2
2
 
3
3
  module CZTop
4
4
 
5
- VERSION = '2.0.2'
5
+ VERSION = '2.0.4'
6
6
 
7
7
  end
@@ -145,7 +145,15 @@ module CZTop
145
145
  # @return [String] current socket identity
146
146
  #
147
147
  def identity
148
- Zsock.identity(self).read_string
148
+ ptr = Zsock.identity(self)
149
+ HasFFIDelegate.raise_zmq_err if ptr.null?
150
+ begin
151
+ ptr.read_string
152
+ ensure
153
+ ref = ::FFI::MemoryPointer.new(:pointer)
154
+ ref.write_pointer(ptr)
155
+ CZMQ::FFI.zstr_free(ref)
156
+ end
149
157
  end
150
158
 
151
159
 
data/lib/cztop.rb CHANGED
@@ -40,5 +40,5 @@ require_relative 'cztop/monitor'
40
40
  require_relative 'cztop/curve'
41
41
  require_relative 'cztop/curve/auth'
42
42
 
43
- # Convenience alias — allows `Cztop::Socket::REQ` alongside `CZTop::Socket::REQ`.
43
+ # Convenience alias; allows `Cztop::Socket::REQ` alongside `CZTop::Socket::REQ`.
44
44
  Cztop = CZTop
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cztop
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.2
4
+ version: 2.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrik Wenger
@@ -198,7 +198,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
198
198
  - !ruby/object:Gem::Version
199
199
  version: '0'
200
200
  requirements: []
201
- rubygems_version: 4.0.6
201
+ rubygems_version: 4.0.20
202
202
  specification_version: 4
203
203
  summary: CZMQ FFI binding to bring ZMQ sockets to Ruby
204
204
  test_files: []