omq 0.19.3 → 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: 77ae2b0a529e29f7c7c48478db4b9b0d81955b28dca48cd17c4f5dfb1dbfd118
4
- data.tar.gz: 2ec4f56b9893527d6458e366835878902d3ec254893de2fa0e7579d52be73438
3
+ metadata.gz: 6800afbb9c1e8dd9f7a973f7105ff94a13bf99bc4f6a5c302fc3ff7c2ee3220a
4
+ data.tar.gz: a166c7b6d54596565574b24183d64c649b8a58c8d826bf3074eb800cb9f0f52f
5
5
  SHA512:
6
- metadata.gz: af73f9a2031c776167a616fe2e63775cd036c6f02f24cdb65059a4ffb81572e2747a9a4f662b96bd7574ecac3d48114f7ea3569bfbc1f6f3bc0f571a523df1ca
7
- data.tar.gz: e919e6bce582848096c692a76e63081b038fb77a9796e914f2265c1825d36017278d46b014b639bbc100935be3ae82af1ac1bfde29951e49e2643ac942d6c251
6
+ metadata.gz: 50c173ee88a276291a82864adadeb11500265d3df24164de183407af0b1586498105b4a837a75027f2762b5a91a9f70ad6dbf0a718f9dc179e920ba3a238e266
7
+ data.tar.gz: da22b101249f108c9dc59763f1589fb1cd350b73bdbd62a3f59424079db68cb892bc1a93052cab603ae0f62eefb35fee1ff3a10743f500ed68dab622d3a3a027
data/CHANGELOG.md CHANGED
@@ -1,5 +1,55 @@
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
+
3
53
  ## 0.19.3 — 2026-04-13
4
54
 
5
55
  ### Changed
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
@@ -104,7 +104,19 @@ module OMQ
104
104
  recv_queue.enqueue(msg)
105
105
 
106
106
  count += 1
107
- bytes += msg.sum { |part| part.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
108
120
  end
109
121
 
110
122
  task.yield
@@ -132,13 +144,28 @@ module OMQ
132
144
  loop do
133
145
  count = 0
134
146
  bytes = 0
147
+
135
148
  while count < FAIRNESS_MESSAGES && bytes < FAIRNESS_BYTES
136
149
  msg = conn.receive_message
137
150
  engine.emit_verbose_msg_received(conn, msg)
138
151
  recv_queue.enqueue(msg)
152
+
139
153
  count += 1
140
- bytes += msg.sum { |part| part.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
141
167
  end
168
+
142
169
  task.yield
143
170
  end
144
171
  rescue Async::Stop, Async::Cancel
data/lib/omq/engine.rb CHANGED
@@ -587,6 +587,10 @@ module OMQ
587
587
  break if deadline && (deadline - Async::Clock.now) <= 0
588
588
  sleep 0.001
589
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.
590
594
  end
591
595
 
592
596
 
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
data/lib/omq/push_pull.rb CHANGED
@@ -7,15 +7,18 @@ 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
11
  # @param send_hwm [Integer, nil] send high water mark (nil uses default)
12
12
  # @param send_timeout [Numeric, nil] send timeout in seconds
13
13
  # @param backend [Symbol, nil] :ruby (default) or :ffi
14
14
  #
15
- def initialize(endpoints = nil, linger: 0, send_hwm: nil, send_timeout: nil, backend: nil)
16
- init_engine(:PUSH, linger: linger, send_hwm: send_hwm, send_timeout: send_timeout, backend: backend)
15
+ def initialize(endpoints = nil, linger: Float::INFINITY, send_hwm: nil, send_timeout: nil, backend: nil, &block)
16
+ init_engine(:PUSH, send_hwm: send_hwm, send_timeout: send_timeout, backend: backend)
17
+ @options.linger = linger
17
18
  attach_endpoints(endpoints, default: :connect)
19
+ finalize_init(&block)
18
20
  end
21
+
19
22
  end
20
23
 
21
24
 
@@ -25,14 +28,16 @@ module OMQ
25
28
  include Readable
26
29
 
27
30
  # @param endpoints [String, nil] endpoint to bind/connect
28
- # @param linger [Integer] linger period in seconds
29
31
  # @param recv_hwm [Integer, nil] receive high water mark (nil uses default)
30
32
  # @param recv_timeout [Numeric, nil] receive timeout in seconds
31
33
  # @param backend [Symbol, nil] :ruby (default) or :ffi
32
34
  #
33
- def initialize(endpoints = nil, linger: 0, recv_hwm: nil, recv_timeout: nil, backend: nil)
34
- init_engine(:PULL, linger: linger, recv_hwm: recv_hwm, recv_timeout: recv_timeout, backend: backend)
35
+ def initialize(endpoints = nil, recv_hwm: nil, recv_timeout: nil, backend: nil, &block)
36
+ init_engine(:PULL, recv_hwm: recv_hwm, recv_timeout: recv_timeout, backend: backend)
35
37
  attach_endpoints(endpoints, default: :bind)
38
+ finalize_init(&block)
36
39
  end
40
+
37
41
  end
42
+
38
43
  end
data/lib/omq/req_rep.rb CHANGED
@@ -8,13 +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
13
+ # @param recv_hwm [Integer, nil] receive high water mark
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(:REQ, 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(:REQ, 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
19
31
 
20
32
 
@@ -25,12 +37,25 @@ module OMQ
25
37
  include Writable
26
38
 
27
39
  # @param endpoints [String, nil] endpoint to bind/connect
28
- # @param linger [Integer] linger period in seconds
40
+ # @param linger [Numeric] linger period in seconds (Float::INFINITY = wait forever, 0 = drop)
41
+ # @param send_hwm [Integer, nil] send high water mark
42
+ # @param recv_hwm [Integer, nil] receive high water mark
43
+ # @param send_timeout [Numeric, nil] send timeout in seconds
44
+ # @param recv_timeout [Numeric, nil] receive timeout in seconds
29
45
  # @param backend [Symbol, nil] :ruby (default) or :ffi
30
46
  #
31
- def initialize(endpoints = nil, linger: 0, backend: nil)
32
- init_engine(:REP, linger: linger, backend: backend)
47
+ def initialize(endpoints = nil, linger: Float::INFINITY,
48
+ send_hwm: nil, recv_hwm: nil,
49
+ send_timeout: nil, recv_timeout: nil,
50
+ backend: nil, &block)
51
+ init_engine(:REP, send_hwm: send_hwm, recv_hwm: recv_hwm,
52
+ send_timeout: send_timeout, recv_timeout: recv_timeout,
53
+ backend: backend)
54
+ @options.linger = linger
33
55
  attach_endpoints(endpoints, default: :bind)
56
+ finalize_init(&block)
34
57
  end
58
+
35
59
  end
60
+
36
61
  end
@@ -8,13 +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
13
+ # @param recv_hwm [Integer, nil] receive high water mark
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(:DEALER, 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(:DEALER, 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
19
31
 
20
32
 
@@ -25,12 +37,23 @@ module OMQ
25
37
  include Writable
26
38
 
27
39
  # @param endpoints [String, nil] endpoint to bind/connect
28
- # @param linger [Integer] linger period in seconds
40
+ # @param linger [Numeric] linger period in seconds (Float::INFINITY = wait forever, 0 = drop)
41
+ # @param send_hwm [Integer, nil] send high water mark
42
+ # @param recv_hwm [Integer, nil] receive high water mark
43
+ # @param send_timeout [Numeric, nil] send timeout in seconds
44
+ # @param recv_timeout [Numeric, nil] receive timeout in seconds
29
45
  # @param backend [Symbol, nil] :ruby (default) or :ffi
30
46
  #
31
- def initialize(endpoints = nil, linger: 0, backend: nil)
32
- init_engine(:ROUTER, linger: linger, backend: backend)
47
+ def initialize(endpoints = nil, linger: Float::INFINITY,
48
+ send_hwm: nil, recv_hwm: nil,
49
+ send_timeout: nil, recv_timeout: nil,
50
+ backend: nil, &block)
51
+ init_engine(:ROUTER, send_hwm: send_hwm, recv_hwm: recv_hwm,
52
+ send_timeout: send_timeout, recv_timeout: recv_timeout,
53
+ backend: backend)
54
+ @options.linger = linger
33
55
  attach_endpoints(endpoints, default: :bind)
56
+ finalize_init(&block)
34
57
  end
35
58
 
36
59
 
@@ -44,5 +67,7 @@ module OMQ
44
67
  parts = message.is_a?(Array) ? message : [message]
45
68
  send([receiver, '', *parts])
46
69
  end
70
+
47
71
  end
72
+
48
73
  end
@@ -124,6 +124,12 @@ module OMQ
124
124
  end
125
125
  end
126
126
 
127
+ # Single-connection fast path: skip Enumerator#next entirely.
128
+ # The vast majority of sockets have exactly one peer.
129
+ if @queues.size == 1
130
+ return @queues.first.dequeue(timeout: 0)
131
+ end
132
+
127
133
  @queues.size.times do
128
134
  q = begin
129
135
  @cycle.next
@@ -96,7 +96,7 @@ module OMQ
96
96
  Routing.drain_send_queue(@send_queue, batch)
97
97
 
98
98
  if batch.size == 1
99
- conn.write_message(batch[0])
99
+ conn.write_message(batch.first)
100
100
  else
101
101
  conn.write_messages(batch)
102
102
  end
@@ -158,7 +158,7 @@ module OMQ
158
158
 
159
159
 
160
160
  def drain_send_queue_capped(batch)
161
- bytes = batch_bytes(batch[0])
161
+ bytes = batch_bytes(batch.first)
162
162
  while batch.size < BATCH_MSG_CAP && bytes < BATCH_BYTE_CAP
163
163
  msg = @send_queue.dequeue(timeout: 0)
164
164
  break unless msg
@@ -174,13 +174,25 @@ module OMQ
174
174
  # fairness cap rather than crashing on #bytesize.
175
175
  #
176
176
  def batch_bytes(parts)
177
- parts.sum { |p| p.respond_to?(:bytesize) ? p.bytesize : 0 }
177
+ if parts.size == 1
178
+ p = parts.first
179
+ p.respond_to?(:bytesize) ? p.bytesize : 0
180
+ else
181
+ total = 0
182
+ i, n = 0, parts.size
183
+ while i < n
184
+ p = parts[i]
185
+ total += p.bytesize if p.respond_to?(:bytesize)
186
+ i += 1
187
+ end
188
+ total
189
+ end
178
190
  end
179
191
 
180
192
 
181
193
  def write_batch(conn, batch)
182
194
  if batch.size == 1
183
- conn.send_message(transform_send(batch[0]))
195
+ conn.send_message(transform_send(batch.first))
184
196
  else
185
197
  conn.write_messages(batch)
186
198
  conn.flush
data/lib/omq/socket.rb CHANGED
@@ -73,9 +73,28 @@ module OMQ
73
73
 
74
74
  # @param endpoints [String, nil] optional endpoint with prefix convention
75
75
  # (+@+ for bind, +>+ for connect, plain uses subclass default)
76
- # @param linger [Integer] linger period in seconds (default 0)
77
76
  #
78
- def initialize(endpoints = nil, linger: 0)
77
+ # @yieldparam [self] the socket, when a block is passed; the socket
78
+ # is {#close}d when the block returns (or raises).
79
+ #
80
+ # Use option accessors (e.g. +socket.linger = 0+) to configure
81
+ # post-construction.
82
+ #
83
+ def initialize(endpoints = nil)
84
+ end
85
+
86
+
87
+ # Yields +self+ if a block was given, then closes. Called by every
88
+ # subclass initializer so +OMQ::PUSH.new { |p| ... }+ behaves like
89
+ # +File.open+: configure, use, auto-close.
90
+ #
91
+ def finalize_init
92
+ return unless block_given?
93
+ begin
94
+ yield self
95
+ ensure
96
+ close
97
+ end
79
98
  end
80
99
 
81
100
 
@@ -294,12 +313,11 @@ module OMQ
294
313
  # subclass initializers (including out-of-tree socket types).
295
314
  #
296
315
  # @param socket_type [Symbol]
297
- # @param linger [Integer]
298
316
  #
299
- def init_engine(socket_type, linger:, send_hwm: nil, recv_hwm: nil,
317
+ def init_engine(socket_type, send_hwm: nil, recv_hwm: nil,
300
318
  send_timeout: nil, recv_timeout: nil, conflate: false,
301
319
  on_mute: nil, backend: nil)
302
- @options = Options.new(linger: linger)
320
+ @options = Options.new
303
321
  @options.send_hwm = send_hwm if send_hwm
304
322
  @options.recv_hwm = recv_hwm if recv_hwm
305
323
  @options.send_timeout = send_timeout if send_timeout
data/lib/omq/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OMQ
4
- VERSION = "0.19.3"
4
+ VERSION = "0.20.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omq
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.19.3
4
+ version: 0.20.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrik Wenger