omq 0.19.2 → 0.20.0

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: 19b283aa20e543617cd7be9c899d2954c219631fd57417536be4fb26798e6e85
4
- data.tar.gz: a39d1c870e6171726a00a5f1809556e3ce7c3a8c0e4b0e84722eae15f83f93b6
3
+ metadata.gz: 6800afbb9c1e8dd9f7a973f7105ff94a13bf99bc4f6a5c302fc3ff7c2ee3220a
4
+ data.tar.gz: a166c7b6d54596565574b24183d64c649b8a58c8d826bf3074eb800cb9f0f52f
5
5
  SHA512:
6
- metadata.gz: 7a020d02b2a1da25dd37f6ff92836348985fb7bcd7b8c17392bacb4c23d0ee443b2dd294f8e30d5df5724e11049462d428efb6d2b5a3313b479ab7a3e7e71388
7
- data.tar.gz: 3b9724342594f22aabe00d1b13d7aa71bfe652ca21954c6b4789556e95743fd03d1c62214a164be8cad717405766a5d0acdbb4c3fd73a0f95aa14738cfb17d81
6
+ metadata.gz: 50c173ee88a276291a82864adadeb11500265d3df24164de183407af0b1586498105b4a837a75027f2762b5a91a9f70ad6dbf0a718f9dc179e920ba3a238e266
7
+ data.tar.gz: da22b101249f108c9dc59763f1589fb1cd350b73bdbd62a3f59424079db68cb892bc1a93052cab603ae0f62eefb35fee1ff3a10743f500ed68dab622d3a3a027
data/CHANGELOG.md CHANGED
@@ -1,5 +1,73 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.20.0 — 2026-04-14
4
+
5
+ ### Changed
6
+
7
+ - **Default `linger` is now `Float::INFINITY`** (matches libzmq). Sockets
8
+ wait forever on close for queued messages to drain unless `linger` is
9
+ set explicitly. Pass `linger: 0` to keep the old "drop on close"
10
+ behavior. `Options#linger` now always returns a `Numeric` (never `nil`).
11
+ - **Socket constructors accept a block.** `OMQ::PUSH.new { |p| ... }`
12
+ yields the socket, then closes it (even on exception) — `File.open`
13
+ style. Applies to every socket type.
14
+ - **Per-socket-type constructors take the full kwarg set** they support:
15
+ `send_hwm`, `recv_hwm`, `send_timeout`, `recv_timeout`, `linger`,
16
+ `backend`, plus pattern-specific ones (`subscribe:`, `on_mute:`,
17
+ `conflate:`). Previously some only accepted `linger`.
18
+ - **Hot-path recv pump: size-1 fast path for byte counting.** The
19
+ `FAIRNESS_BYTES` accumulator in `RecvPump#start_direct` (and its
20
+ transform variant) now short-circuits single-frame messages instead
21
+ of iterating, keeping both entry methods monomorphic for YJIT.
22
+ - **Hot-path round-robin `batch_bytes`** short-circuits single-frame
23
+ batches the same way, replacing `parts.sum { ... }` with a direct
24
+ `bytesize` call.
25
+ - **Fair-queue single-connection fast path.** `try_dequeue` now skips
26
+ `Enumerator#next` when a fair-queue recv socket has exactly one peer
27
+ (the common case) and dequeues directly from the sole queue.
28
+ - **`drain_send_queues` is cancellation-safe.** `Async::Stop` raised at
29
+ the drain sleep point (e.g. from a parent `task.stop`) is now rescued
30
+ so `Socket#close` can finish the rest of its teardown instead of
31
+ propagating the cancellation out of the ensure path.
32
+ - **Hot-path `Array#[0]` → `Array#first`** in writable batching and
33
+ pair routing — `#first` has a dedicated YJIT specialization that is
34
+ measurably faster on single-frame messages.
35
+ - **Benchmark size sweep reworked.** `SIZES` is now a ×4 geometric
36
+ progression `128, 512, 2048, 8192, 32_768` bytes, replacing
37
+ `64 / 1024 / 8192 / 65_536`. Fills the 64 B → 1 KiB gap, drops 64 KiB
38
+ (tcp/ipc already saturated at 32 KiB, inproc regressed). `report.rb
39
+ --update-readme` and `bench/README.md` regenerated.
40
+
41
+ ### Fixed
42
+
43
+ - **Slow `send_timeout` test.** The `raises IO::TimeoutError when send
44
+ blocks longer than send_timeout` test now constructs its PUSH with
45
+ `linger: 0`. Previously the undeliverable fill message combined with
46
+ the new default `linger: Float::INFINITY` made the close-in-ensure
47
+ path wait out the full linger budget, silently eating the enclosing
48
+ `task.with_timeout` and inflating suite runtime.
49
+ - **Test suite runtime.** `TEST_ASYNC_TIMEOUT` lowered from 5 s to 1 s:
50
+ real hangs fail fast and the full suite finishes in ~3 s instead of
51
+ ~8 s.
52
+
53
+ ## 0.19.3 — 2026-04-13
54
+
55
+ ### Changed
56
+
57
+ - Engine no longer reaches into `routing.recv_queue` directly.
58
+ Routing strategies now expose `#dequeue_recv` and `#unblock_recv`
59
+ as the engine-facing recv contract. `FairRecv` provides the
60
+ shared implementation for fair-queued sockets; sub/xsub/xpub
61
+ delegate inline; write-only push/pub raise on dequeue and no-op
62
+ on unblock. Sharpens the routing interface and keeps Engine out
63
+ of queue internals.
64
+ - `Writable#freeze_message` collapsed: single `all?` predicate
65
+ check drives three outcomes (already-frozen-array fast path,
66
+ freeze-in-place, convert-via-map/map!) instead of mirrored
67
+ fast/slow branches that each repeated the predicate.
68
+ - Hot-path optimized. Avoid the overhead of `parts.sum(&:bytesize)`
69
+ and use `parts.sum { |p| p.bytesize }` instead.
70
+
3
71
  ## 0.19.2 — 2026-04-13
4
72
 
5
73
  ### Added
data/README.md CHANGED
@@ -1,41 +1,32 @@
1
- # OMQWhere did the C dependency go!?
1
+ # ØMQZeroMQ for Ruby, no C required
2
2
 
3
3
  [![CI](https://github.com/zeromq/omq/actions/workflows/ci.yml/badge.svg)](https://github.com/zeromq/omq/actions/workflows/ci.yml)
4
4
  [![Gem Version](https://img.shields.io/gem/v/omq?color=e9573f)](https://rubygems.org/gems/omq)
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
- `gem install omq` — that's it. No libzmq, no compiler, no system packages. Just Ruby.
9
-
10
- OMQ builds ZeroMQ socket patterns on top of [protocol-zmtp](https://github.com/paddor/protocol-zmtp) (a pure Ruby [ZMTP 3.1](https://rfc.zeromq.org/spec/23/) codec) using [Async](https://github.com/socketry/async) fibers. It speaks native ZeroMQ on the wire and interoperates with libzmq, pyzmq, CZMQ, and everything else in the ZMQ ecosystem.
11
-
12
- > **980k msg/s** inproc | **38k msg/s** ipc | **31k msg/s** tcp
8
+ > **932k msg/s** inproc | **328k msg/s** ipc | **329k msg/s** tcp
13
9
  >
14
- > **10 µs** inproc latency | **71 µs** ipc | **82 µs** tcp
10
+ > **11.5 µs** inproc latency | **54 µs** ipc | **69 µs** tcp
15
11
  >
16
12
  > Ruby 4.0 + YJIT on a Linux VM — see [`bench/`](bench/) for full results
17
13
 
18
- ---
19
-
20
- ## What is ZeroMQ?
21
-
22
- Brokerless message-oriented middleware. No central server, no extra hop — processes talk directly to each other, cutting latency in half compared to broker-based systems. You get the patterns you'd normally build on top of RabbitMQ or Redis — pub/sub, work distribution, request/reply, fan-out — but decentralized, with no single point of failure.
23
-
24
- Networking is hard. ZeroMQ abstracts away reconnection, queuing, load balancing, and framing so you can focus on what your system actually does. Start with threads talking over `inproc://`, split into processes with `ipc://`, scale across machines with `tcp://` — same code, same API, just change the URL.
14
+ `gem install omq` and you're done. No libzmq, no compiler, no system packages — just Ruby talking to every other ZeroMQ peer out there.
25
15
 
26
- If you've ever wired up services with raw TCP, HTTP polling, or Redis pub/sub and wished it was simpler, this is what you've been looking for.
16
+ ØMQ gives your Ruby processes a way to talk to each other — and to anything else speaking ZeroMQ — without a broker in the middle. Same API whether they live in the same process, on the same machine, or across the network. Reconnects, queuing, and back-pressure are handled for you; you write the interesting part.
27
17
 
28
- See [GETTING_STARTED.md](GETTING_STARTED.md) for a ~30 min walkthrough of all major patterns with working code.
18
+ New to ZeroMQ? Start with [GETTING_STARTED.md](GETTING_STARTED.md) a ~30 min walkthrough of every major pattern with working code.
29
19
 
30
20
  ## Highlights
31
21
 
32
22
  - **Zero dependencies on C** — no extensions, no FFI, no libzmq. `gem install` just works everywhere
33
- - **Fast** — YJIT-optimized hot paths, batched sends, recv prefetching, direct-pipe inproc bypass. 980k msg/s inproc, 10 µs latency
34
- - **[`omq` CLI](https://github.com/paddor/omq-cli)** — `gem install omq-cli` for a command-line tool with Ruby eval, Ractor parallelism, and script handlers
23
+ - **Fast** — YJIT-optimized hot paths, batched sends, GC-tuned allocations, buffered I/O via [io-stream](https://github.com/socketry/io-stream), direct-pipe inproc bypass
24
+ - **[`omq` CLI](https://github.com/paddor/omq-cli)** — a powerful swiss army knife for ØMQ. `gem install omq-cli`
35
25
  - **Every socket pattern** — req/rep, pub/sub, push/pull, dealer/router, xpub/xsub, pair, and all draft types
36
26
  - **Every transport** — tcp, ipc (Unix domain sockets), inproc (in-process queues)
37
- - **Async-native** — built on fibers, non-blocking from the ground up. A shared IO thread handles sockets outside of Async — no reactor needed for simple scripts
38
- - **Wire-compatible** — interoperates with libzmq, pyzmq, CZMQ over tcp and ipc
27
+ - **Async-native** — built on fibers, non-blocking from the ground up
28
+ - **Works outside Async too** — a shared IO thread handles sockets for callers that aren't inside a reactor, so simple scripts just work
29
+ - **Wire-compatible** — interoperates with libzmq, pyzmq, CZMQ, zmq.rs over tcp and ipc
39
30
  - **Bind/connect order doesn't matter** — connect before bind, bind before connect, peers come and go. ZeroMQ reconnects automatically and queued messages drain when peers arrive
40
31
 
41
32
  For architecture internals, see [DESIGN.md](DESIGN.md).
@@ -164,15 +155,15 @@ All sockets are thread-safe. Default HWM is 1000 messages per socket. `max_messa
164
155
 
165
156
  #### Draft (single-frame only)
166
157
 
167
- These require the `omq-draft` gem.
158
+ Each draft pattern lives in its own gem — install only the ones you use.
168
159
 
169
- | Pattern | Send | Receive | When HWM full |
170
- |---------|------|---------|---------------|
171
- | **CLIENT** / **SERVER** | Work-stealing / routing-ID | Fair-queue | Block |
172
- | **RADIO** / **DISH** | Group fan-out | Group filter | Drop |
173
- | **SCATTER** / **GATHER** | Work-stealing | Fair-queue | Block |
174
- | **PEER** | Routing-ID | Fair-queue | Block |
175
- | **CHANNEL** | Exclusive 1-to-1 | Exclusive 1-to-1 | Block |
160
+ | Pattern | Send | Receive | When HWM full | Gem |
161
+ |---------|------|---------|---------------|-----|
162
+ | **CLIENT** / **SERVER** | Work-stealing / routing-ID | Fair-queue | Block | [`omq-rfc-clientserver`](https://github.com/paddor/omq-rfc-clientserver) |
163
+ | **RADIO** / **DISH** | Group fan-out | Group filter | Drop | [`omq-rfc-radiodish`](https://github.com/paddor/omq-rfc-radiodish) |
164
+ | **SCATTER** / **GATHER** | Work-stealing | Fair-queue | Block | [`omq-rfc-scattergather`](https://github.com/paddor/omq-rfc-scattergather) |
165
+ | **PEER** | Routing-ID | Fair-queue | Block | [`omq-rfc-p2p`](https://github.com/paddor/omq-rfc-p2p) |
166
+ | **CHANNEL** | Exclusive 1-to-1 | Exclusive 1-to-1 | Block | [`omq-rfc-channel`](https://github.com/paddor/omq-rfc-channel) |
176
167
 
177
168
  ## CLI
178
169
 
@@ -192,6 +183,12 @@ See the [omq-cli README](https://github.com/paddor/omq-cli) for full documentati
192
183
  - **[omq-ffi](https://github.com/paddor/omq-ffi)** — libzmq FFI backend. Same OMQ socket API, but backed by libzmq instead of the pure Ruby ZMTP stack. Useful for interop testing and when you need libzmq-specific features. Requires libzmq installed.
193
184
  - **[omq-ractor](https://github.com/paddor/omq-ractor)** — bridge OMQ sockets into Ruby Ractors for true parallel processing across cores. I/O stays on the main Ractor, worker Ractors do pure computation.
194
185
 
186
+ ### Protocol extensions (RFCs)
187
+
188
+ Optional plug-ins that extend the ZMTP wire protocol. Each is a separate gem; load the ones you need.
189
+
190
+ - **[omq-rfc-zstd](https://github.com/paddor/omq-rfc-zstd)** — transparent Zstandard compression on the wire, negotiated per peer via READY properties.
191
+
195
192
  ## Development
196
193
 
197
194
  ```sh
@@ -210,16 +207,16 @@ the stack.
210
207
  # clone OMQ and its sibling repos into the same parent directory
211
208
  git clone https://github.com/paddor/omq.git
212
209
  git clone https://github.com/paddor/protocol-zmtp.git
213
- git clone https://github.com/paddor/nuckle.git
214
- git clone https://github.com/paddor/omq-rfc-blake3zmq.git
215
- git clone https://github.com/paddor/omq-rfc-channel.git
210
+ git clone https://github.com/paddor/omq-rfc-zstd.git
216
211
  git clone https://github.com/paddor/omq-rfc-clientserver.git
217
- git clone https://github.com/paddor/omq-rfc-p2p.git
218
- git clone https://github.com/paddor/omq-rfc-qos.git
219
212
  git clone https://github.com/paddor/omq-rfc-radiodish.git
220
213
  git clone https://github.com/paddor/omq-rfc-scattergather.git
214
+ git clone https://github.com/paddor/omq-rfc-channel.git
215
+ git clone https://github.com/paddor/omq-rfc-p2p.git
216
+ git clone https://github.com/paddor/omq-rfc-qos.git
221
217
  git clone https://github.com/paddor/omq-ffi.git
222
218
  git clone https://github.com/paddor/omq-ractor.git
219
+ git clone https://github.com/paddor/nuckle.git
223
220
 
224
221
  cd omq
225
222
  OMQ_DEV=1 bundle install
@@ -91,18 +91,34 @@ module OMQ
91
91
  loop do
92
92
  count = 0
93
93
  bytes = 0
94
+
94
95
  while count < FAIRNESS_MESSAGES && bytes < FAIRNESS_BYTES
95
96
  msg = conn.receive_message
96
97
  msg = transform.call(msg).freeze
98
+
97
99
  # Emit the verbose trace BEFORE enqueueing so the monitor
98
100
  # fiber is woken before the application fiber -- the
99
101
  # async scheduler is FIFO on the ready list, so this
100
102
  # preserves log-before-stdout ordering for -vvv traces.
101
103
  engine.emit_verbose_msg_received(conn, msg)
102
104
  recv_queue.enqueue(msg)
105
+
103
106
  count += 1
104
- bytes += msg.sum(&:bytesize) if count_bytes
107
+
108
+ # hot path
109
+ if count_bytes
110
+ if msg.size == 1
111
+ bytes += msg.first.bytesize
112
+ else
113
+ i, n = 0, msg.size
114
+ while i < n
115
+ bytes += msg[i].bytesize
116
+ i += 1
117
+ end
118
+ end
119
+ end
105
120
  end
121
+
106
122
  task.yield
107
123
  end
108
124
  rescue Async::Stop, Async::Cancel
@@ -128,13 +144,28 @@ module OMQ
128
144
  loop do
129
145
  count = 0
130
146
  bytes = 0
147
+
131
148
  while count < FAIRNESS_MESSAGES && bytes < FAIRNESS_BYTES
132
149
  msg = conn.receive_message
133
150
  engine.emit_verbose_msg_received(conn, msg)
134
151
  recv_queue.enqueue(msg)
152
+
135
153
  count += 1
136
- bytes += msg.sum(&:bytesize) if count_bytes
154
+
155
+ # hot path
156
+ if count_bytes
157
+ if msg.size == 1
158
+ bytes += msg.first.bytesize
159
+ else
160
+ i, n = 0, msg.size
161
+ while i < n
162
+ bytes += msg[i].bytesize
163
+ i += 1
164
+ end
165
+ end
166
+ end
137
167
  end
168
+
138
169
  task.yield
139
170
  end
140
171
  rescue Async::Stop, Async::Cancel
data/lib/omq/engine.rb CHANGED
@@ -80,6 +80,7 @@ module OMQ
80
80
  #
81
81
  attr_reader :connections, :tasks, :lifecycle
82
82
 
83
+
83
84
  # @!attribute [w] monitor_queue
84
85
  # @param value [Async::Queue, nil] queue for monitor events
85
86
  #
@@ -116,7 +117,9 @@ module OMQ
116
117
  def spawn_inproc_retry(endpoint)
117
118
  ri = @options.reconnect_interval
118
119
  ivl = ri.is_a?(Range) ? ri.begin : ri
119
- @tasks << @lifecycle.barrier.async(transient: true, annotation: "inproc reconnect #{endpoint}") do
120
+ ann = "inproc reconnect #{endpoint}"
121
+
122
+ @tasks << @lifecycle.barrier.async(transient: true, annotation: ann) do
120
123
  yield ivl
121
124
  rescue Async::Stop, Async::Cancel
122
125
  end
@@ -134,7 +137,9 @@ module OMQ
134
137
  capture_parent_task(parent: parent)
135
138
  transport = transport_for(endpoint)
136
139
  listener = transport.bind(endpoint, self)
140
+
137
141
  start_accept_loops(listener)
142
+
138
143
  @listeners << listener
139
144
  @last_endpoint = listener.endpoint
140
145
  @last_tcp_port = listener.respond_to?(:port) ? listener.port : nil
@@ -155,6 +160,7 @@ module OMQ
155
160
  capture_parent_task(parent: parent)
156
161
  validate_endpoint!(endpoint)
157
162
  @dialed.add(endpoint)
163
+
158
164
  if endpoint.start_with?("inproc://")
159
165
  # Inproc connect is synchronous and instant
160
166
  transport = transport_for(endpoint)
@@ -188,6 +194,7 @@ module OMQ
188
194
  def unbind(endpoint)
189
195
  listener = @listeners.find { |l| l.endpoint == endpoint }
190
196
  return unless listener
197
+
191
198
  listener.stop
192
199
  @listeners.delete(listener)
193
200
  close_connections_at(endpoint)
@@ -235,8 +242,10 @@ module OMQ
235
242
  #
236
243
  def dequeue_recv
237
244
  raise @fatal_error if @fatal_error
238
- msg = routing.recv_queue.dequeue
245
+
246
+ msg = routing.dequeue_recv
239
247
  raise @fatal_error if msg.nil? && @fatal_error
248
+
240
249
  msg
241
250
  end
242
251
 
@@ -245,7 +254,7 @@ module OMQ
245
254
  # pending {#dequeue_recv} with a nil return value.
246
255
  #
247
256
  def dequeue_recv_sentinel
248
- routing.recv_queue.push(nil)
257
+ routing.unblock_recv
249
258
  end
250
259
 
251
260
 
@@ -274,6 +283,7 @@ module OMQ
274
283
  # pumps when the connection is lost.
275
284
  parent = @connections[conn]&.barrier || @lifecycle.barrier
276
285
  task = RecvPump.start(parent, conn, recv_queue, self, transform)
286
+
277
287
  @tasks << task if task
278
288
  task
279
289
  end
@@ -316,11 +326,20 @@ module OMQ
316
326
  #
317
327
  def close
318
328
  return unless @lifecycle.open?
329
+
319
330
  @lifecycle.start_closing!
320
331
  stop_listeners unless @connections.empty?
321
- drain_send_queues(@options.linger) if @options.linger.nil? || @options.linger > 0
332
+
333
+ if @options.linger.nil? || @options.linger > 0
334
+ drain_send_queues(@options.linger)
335
+ end
336
+
322
337
  @lifecycle.finish_closing!
323
- Reactor.untrack_linger(@options.linger) if @lifecycle.on_io_thread
338
+
339
+ if @lifecycle.on_io_thread
340
+ Reactor.untrack_linger(@options.linger)
341
+ end
342
+
324
343
  stop_listeners
325
344
  tear_down_barrier
326
345
  routing.stop rescue nil
@@ -337,9 +356,14 @@ module OMQ
337
356
  #
338
357
  def stop
339
358
  return unless @lifecycle.alive?
359
+
340
360
  @lifecycle.start_closing! if @lifecycle.open?
341
361
  @lifecycle.finish_closing!
342
- Reactor.untrack_linger(@options.linger) if @lifecycle.on_io_thread
362
+
363
+ if @lifecycle.on_io_thread
364
+ Reactor.untrack_linger(@options.linger)
365
+ end
366
+
343
367
  stop_listeners
344
368
  tear_down_barrier
345
369
  routing.stop rescue nil
@@ -407,8 +431,9 @@ module OMQ
407
431
  #
408
432
  def signal_fatal_error(error)
409
433
  return unless @lifecycle.open?
434
+
410
435
  @fatal_error = build_fatal_error(error)
411
- routing.recv_queue.push(nil) rescue nil
436
+ routing.unblock_recv rescue nil
412
437
  @lifecycle.peer_connected.resolve(nil) rescue nil
413
438
  end
414
439
 
@@ -442,7 +467,10 @@ module OMQ
442
467
  # @param parent [#async, nil] optional Async parent
443
468
  #
444
469
  def capture_parent_task(parent: nil)
445
- return unless @lifecycle.capture_parent_task(parent: parent, linger: @options.linger)
470
+ task = @lifecycle.capture_parent_task(parent: parent, linger: @options.linger)
471
+
472
+ return unless task
473
+
446
474
  Maintenance.start(@lifecycle.barrier, @options.mechanism, @tasks)
447
475
  end
448
476
 
@@ -491,8 +519,13 @@ module OMQ
491
519
  # +last_wire_size_in+.
492
520
  def emit_verbose_msg_received(conn, parts)
493
521
  return unless @verbose_monitor
522
+
494
523
  detail = { parts: parts }
495
- detail[:wire_size] = conn.last_wire_size_in if conn.respond_to?(:last_wire_size_in)
524
+
525
+ if conn.respond_to?(:last_wire_size_in)
526
+ detail[:wire_size] = conn.last_wire_size_in
527
+ end
528
+
496
529
  emit_monitor_event(:message_received, detail: detail)
497
530
  end
498
531
 
@@ -504,9 +537,14 @@ module OMQ
504
537
  # @raise [ArgumentError] if the scheme is not registered
505
538
  #
506
539
  def transport_for(endpoint)
507
- scheme = endpoint[/\A([^:]+):\/\//, 1]
508
- self.class.transports[scheme] or
540
+ scheme = endpoint[/\A([^:]+):\/\//, 1]
541
+ transport = self.class.transports[scheme]
542
+
543
+ unless transport
509
544
  raise ArgumentError, "unsupported transport: #{endpoint}"
545
+ end
546
+
547
+ transport
510
548
  end
511
549
 
512
550
 
@@ -528,6 +566,7 @@ module OMQ
528
566
  ensure
529
567
  lifecycle&.close!
530
568
  end
569
+
531
570
  @tasks << task if task
532
571
  end
533
572
 
@@ -539,11 +578,19 @@ module OMQ
539
578
  # every routing strategy, so it is flagged rather than fixed here.
540
579
  def drain_send_queues(timeout)
541
580
  return unless @routing.respond_to?(:send_queues_drained?)
542
- deadline = timeout ? Async::Clock.now + timeout : nil
581
+
582
+ if timeout
583
+ deadline = Async::Clock.now + timeout
584
+ end
585
+
543
586
  until @routing.send_queues_drained?
544
587
  break if deadline && (deadline - Async::Clock.now) <= 0
545
588
  sleep 0.001
546
589
  end
590
+ rescue Async::Stop
591
+ # Parent task is being cancelled — stop draining and let close
592
+ # proceed with the rest of teardown instead of propagating the
593
+ # cancellation out of the ensure path.
547
594
  end
548
595
 
549
596
 
@@ -554,12 +601,16 @@ module OMQ
554
601
 
555
602
  def validate_endpoint!(endpoint)
556
603
  transport = transport_for(endpoint)
557
- transport.validate_endpoint!(endpoint) if transport.respond_to?(:validate_endpoint!)
604
+
605
+ if transport.respond_to?(:validate_endpoint!)
606
+ transport.validate_endpoint!(endpoint)
607
+ end
558
608
  end
559
609
 
560
610
 
561
611
  def start_accept_loops(listener)
562
612
  return unless listener.respond_to?(:start_accept_loops)
613
+
563
614
  listener.start_accept_loops(@lifecycle.barrier) do |io|
564
615
  handle_accepted(io, endpoint: listener.endpoint)
565
616
  end
data/lib/omq/options.rb CHANGED
@@ -10,9 +10,11 @@ module OMQ
10
10
  DEFAULT_HWM = 1000
11
11
 
12
12
 
13
- # @param linger [Integer] linger period in seconds (default 0)
13
+ # @param linger [Numeric] linger period in seconds on close
14
+ # (default Float::INFINITY = wait forever, matching libzmq).
15
+ # Pass 0 for immediate drop-on-close.
14
16
  #
15
- def initialize(linger: 0)
17
+ def initialize(linger: Float::INFINITY)
16
18
  @send_hwm = DEFAULT_HWM
17
19
  @recv_hwm = DEFAULT_HWM
18
20
  @linger = linger
@@ -39,7 +41,8 @@ module OMQ
39
41
  # @!attribute recv_hwm
40
42
  # @return [Integer] receive high water mark (default 1000, 0 = unbounded)
41
43
  # @!attribute linger
42
- # @return [Integer, nil] linger period in seconds (nil = wait forever, 0 = immediate)
44
+ # @return [Numeric] linger period in seconds on close
45
+ # (Float::INFINITY = wait forever, 0 = immediate drop)
43
46
  # @!attribute identity
44
47
  # @return [String] socket identity for ROUTER addressing (default "")
45
48
  # @!attribute router_mandatory
data/lib/omq/pair.rb CHANGED
@@ -8,12 +8,25 @@ module OMQ
8
8
  include Writable
9
9
 
10
10
  # @param endpoints [String, nil] endpoint to bind/connect
11
- # @param linger [Integer] linger period in seconds
11
+ # @param linger [Numeric] linger period in seconds (Float::INFINITY = wait forever, 0 = drop)
12
+ # @param send_hwm [Integer, nil] send high water mark (nil uses default)
13
+ # @param recv_hwm [Integer, nil] receive high water mark (nil uses default)
14
+ # @param send_timeout [Numeric, nil] send timeout in seconds
15
+ # @param recv_timeout [Numeric, nil] receive timeout in seconds
12
16
  # @param backend [Symbol, nil] :ruby (default) or :ffi
13
17
  #
14
- def initialize(endpoints = nil, linger: 0, backend: nil)
15
- init_engine(:PAIR, linger: linger, backend: backend)
18
+ def initialize(endpoints = nil, linger: Float::INFINITY,
19
+ send_hwm: nil, recv_hwm: nil,
20
+ send_timeout: nil, recv_timeout: nil,
21
+ backend: nil, &block)
22
+ init_engine(:PAIR, send_hwm: send_hwm, recv_hwm: recv_hwm,
23
+ send_timeout: send_timeout, recv_timeout: recv_timeout,
24
+ backend: backend)
25
+ @options.linger = linger
16
26
  attach_endpoints(endpoints, default: :connect)
27
+ finalize_init(&block)
17
28
  end
29
+
18
30
  end
31
+
19
32
  end
data/lib/omq/pub_sub.rb CHANGED
@@ -7,15 +7,23 @@ module OMQ
7
7
  include Writable
8
8
 
9
9
  # @param endpoints [String, nil] endpoint to bind/connect
10
- # @param linger [Integer] linger period in seconds
10
+ # @param linger [Numeric] linger period in seconds (Float::INFINITY = wait forever, 0 = drop)
11
+ # @param send_hwm [Integer, nil] send high water mark
12
+ # @param send_timeout [Numeric, nil] send timeout in seconds
11
13
  # @param on_mute [Symbol] mute strategy for slow subscribers
12
14
  # @param conflate [Boolean] keep only latest message per topic
13
15
  # @param backend [Symbol, nil] :ruby (default) or :ffi
14
16
  #
15
- def initialize(endpoints = nil, linger: 0, on_mute: :drop_newest, conflate: false, backend: nil)
16
- init_engine(:PUB, linger: linger, on_mute: on_mute, conflate: conflate, backend: backend)
17
+ def initialize(endpoints = nil, linger: Float::INFINITY,
18
+ send_hwm: nil, send_timeout: nil,
19
+ on_mute: :drop_newest, conflate: false, backend: nil, &block)
20
+ init_engine(:PUB, send_hwm: send_hwm, send_timeout: send_timeout,
21
+ on_mute: on_mute, conflate: conflate, backend: backend)
22
+ @options.linger = linger
17
23
  attach_endpoints(endpoints, default: :bind)
24
+ finalize_init(&block)
18
25
  end
26
+
19
27
  end
20
28
 
21
29
 
@@ -29,16 +37,21 @@ module OMQ
29
37
  EVERYTHING = ''
30
38
 
31
39
 
32
- # @param endpoints [String, nil]
33
- # @param linger [Integer]
40
+ # @param endpoints [String, nil] endpoint to bind/connect
41
+ # @param recv_hwm [Integer, nil] receive high water mark
42
+ # @param recv_timeout [Numeric, nil] receive timeout in seconds
34
43
  # @param subscribe [String, nil] subscription prefix; +nil+ (default)
35
44
  # means no subscription — call {#subscribe} explicitly.
36
45
  # @param on_mute [Symbol] :block (default), :drop_newest, or :drop_oldest
46
+ # @param backend [Symbol, nil] :ruby (default) or :ffi
37
47
  #
38
- def initialize(endpoints = nil, linger: 0, subscribe: nil, on_mute: :block, backend: nil)
39
- init_engine(:SUB, linger: linger, on_mute: on_mute, backend: backend)
48
+ def initialize(endpoints = nil, recv_hwm: nil, recv_timeout: nil,
49
+ subscribe: nil, on_mute: :block, backend: nil, &block)
50
+ init_engine(:SUB, recv_hwm: recv_hwm, recv_timeout: recv_timeout,
51
+ on_mute: on_mute, backend: backend)
40
52
  attach_endpoints(endpoints, default: :connect)
41
53
  self.subscribe(subscribe) unless subscribe.nil?
54
+ finalize_init(&block)
42
55
  end
43
56
 
44
57
 
@@ -60,6 +73,7 @@ module OMQ
60
73
  def unsubscribe(prefix)
61
74
  @engine.routing.unsubscribe(prefix)
62
75
  end
76
+
63
77
  end
64
78
 
65
79
 
@@ -70,14 +84,26 @@ module OMQ
70
84
  include Writable
71
85
 
72
86
  # @param endpoints [String, nil] endpoint to bind/connect
73
- # @param linger [Integer] linger period in seconds
87
+ # @param linger [Numeric] linger period in seconds (Float::INFINITY = wait forever, 0 = drop)
88
+ # @param send_hwm [Integer, nil] send high water mark
89
+ # @param recv_hwm [Integer, nil] receive high water mark
90
+ # @param send_timeout [Numeric, nil] send timeout in seconds
91
+ # @param recv_timeout [Numeric, nil] receive timeout in seconds
74
92
  # @param on_mute [Symbol] mute strategy for slow subscribers
75
93
  # @param backend [Symbol, nil] :ruby (default) or :ffi
76
94
  #
77
- def initialize(endpoints = nil, linger: 0, on_mute: :drop_newest, backend: nil)
78
- init_engine(:XPUB, linger: linger, on_mute: on_mute, backend: backend)
95
+ def initialize(endpoints = nil, linger: Float::INFINITY,
96
+ send_hwm: nil, recv_hwm: nil,
97
+ send_timeout: nil, recv_timeout: nil,
98
+ on_mute: :drop_newest, backend: nil, &block)
99
+ init_engine(:XPUB, send_hwm: send_hwm, recv_hwm: recv_hwm,
100
+ send_timeout: send_timeout, recv_timeout: recv_timeout,
101
+ on_mute: on_mute, backend: backend)
102
+ @options.linger = linger
79
103
  attach_endpoints(endpoints, default: :bind)
104
+ finalize_init(&block)
80
105
  end
106
+
81
107
  end
82
108
 
83
109
 
@@ -87,17 +113,30 @@ module OMQ
87
113
  include Readable
88
114
  include Writable
89
115
 
90
- # @param endpoints [String, nil]
91
- # @param linger [Integer]
116
+ # @param endpoints [String, nil] endpoint to bind/connect
117
+ # @param linger [Numeric] linger period in seconds (Float::INFINITY = wait forever, 0 = drop)
118
+ # @param send_hwm [Integer, nil] send high water mark
119
+ # @param recv_hwm [Integer, nil] receive high water mark
120
+ # @param send_timeout [Numeric, nil] send timeout in seconds
121
+ # @param recv_timeout [Numeric, nil] receive timeout in seconds
92
122
  # @param subscribe [String, nil] subscription prefix; +nil+ (default)
93
123
  # means no subscription — send a subscribe frame explicitly.
94
124
  # @param on_mute [Symbol] mute strategy (:block, :drop_newest, :drop_oldest)
95
125
  # @param backend [Symbol, nil] :ruby (default) or :ffi
96
126
  #
97
- def initialize(endpoints = nil, linger: 0, subscribe: nil, on_mute: :block, backend: nil)
98
- init_engine(:XSUB, linger: linger, on_mute: on_mute, backend: backend)
127
+ def initialize(endpoints = nil, linger: Float::INFINITY,
128
+ send_hwm: nil, recv_hwm: nil,
129
+ send_timeout: nil, recv_timeout: nil,
130
+ subscribe: nil, on_mute: :block, backend: nil, &block)
131
+ init_engine(:XSUB, send_hwm: send_hwm, recv_hwm: recv_hwm,
132
+ send_timeout: send_timeout, recv_timeout: recv_timeout,
133
+ on_mute: on_mute, backend: backend)
134
+ @options.linger = linger
99
135
  attach_endpoints(endpoints, default: :connect)
100
136
  send("\x01#{subscribe}".b) unless subscribe.nil?
137
+ finalize_init(&block)
101
138
  end
139
+
102
140
  end
141
+
103
142
  end