Skip to content

Commit c34518a

Browse files
committed
v0.24.0
Caller-owns-data contract: Writable#send no longer deep-freezes or binary-coerces message parts, and Connection#receive_message returns mutable arrays of mutable strings. Non-string parts now NoMethodError at the wire layer instead of being silently .to_s-coerced; nil parts no longer become empty binary frames. Reactor fast path: when a socket was bound/connected from an Async fiber, #send and #receive skip Reactor.run entirely and call the engine directly, with Async::Task#with_timeout only when a timeout is configured. The shared IO thread is used only for non-Async callers. Inproc PUSH/PULL roughly doubles (+105% to +128% single peer, +63% to +111% three peers). IPC gains 5-17%. TCP is unchanged -- already OS/syscall-bound.
1 parent b6a8b04 commit c34518a

15 files changed

Lines changed: 63 additions & 369 deletions

File tree

CHANGELOG.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,41 @@
11
# Changelog
22

3+
## 0.24.0 — 2026-04-18
4+
5+
### Changed
6+
7+
- **Caller owns message parts.** `Writable#send` no longer deep-freezes or
8+
binary-coerces the caller's input. The contract is now libzmq-style:
9+
don't mutate parts after sending. `#receive` likewise returns mutable
10+
arrays of mutable strings. This removes a full-payload allocation per
11+
message (`.b.freeze`) on the send path and a per-frame freeze on the
12+
receive path.
13+
14+
- **No more implicit `#to_s` / nil coercion.** Passing a non-string part
15+
(e.g. Integer, Symbol, nil) will raise `NoMethodError` at the wire layer
16+
instead of being silently converted. The `EMPTY_PART` constant is gone.
17+
18+
- **Reactor fast path for `#send` / `#receive`.** When the socket was
19+
bound/connected from an Async fiber, hot-path I/O skips `Reactor.run`
20+
entirely and calls the engine directly (with an `Async::Task#with_timeout`
21+
wrapper only when a timeout is configured). The shared IO thread is used
22+
only when the socket was created from a non-Async thread.
23+
24+
### Performance
25+
26+
Combined effect of caller-owns-data + Reactor fast path on inproc:
27+
28+
- PUSH/PULL inproc 1-peer: **+105% to +128%** msg/s across payload sizes
29+
- PUSH/PULL inproc 3-peer: **+63% to +111%** msg/s
30+
- PUSH/PULL ipc: +5% to +17%
31+
- TCP numbers unchanged (OS/syscall-dominated)
32+
33+
### Removed
34+
35+
- `Writable#freeze_message` and `#frozen_binary` private helpers.
36+
- `Writable::EMPTY_PART` constant.
37+
38+
339
## 0.23.1 — 2026-04-18
440

541
### Fixed

lib/omq/client_server.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def initialize(endpoints = nil, linger: Float::INFINITY, backend: nil)
6060
# @return [self]
6161
#
6262
def send_to(routing_id, message)
63-
parts = [routing_id.b.freeze, message.b.freeze]
63+
parts = [routing_id, message]
6464
Reactor.run(timeout: @options.write_timeout) { @engine.enqueue_send(parts) }
6565
self
6666
end

lib/omq/engine.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ def all_peers_gone = @lifecycle.all_peers_gone
100100
def parent_task = @lifecycle.parent_task
101101
def barrier = @lifecycle.barrier
102102
def closed? = @lifecycle.closed?
103+
def on_io_thread? = @lifecycle.on_io_thread
103104

104105

105106
# Enables or disables auto-reconnect for dropped connections.

lib/omq/engine/recv_pump.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ def start_with_transform(parent, transform)
9494

9595
while count < FAIRNESS_MESSAGES && bytes < FAIRNESS_BYTES
9696
msg = conn.receive_message
97-
msg = transform.call(msg).freeze
97+
msg = transform.call(msg)
9898

9999
# Emit the verbose trace BEFORE enqueueing so the monitor
100100
# fiber is woken before the application fiber -- the

lib/omq/peer.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def initialize(endpoints = nil, linger: Float::INFINITY, backend: nil)
3838
# @return [self]
3939
#
4040
def send_to(routing_id, message)
41-
parts = [routing_id.b.freeze, message.b.freeze]
41+
parts = [routing_id, message]
4242
Reactor.run(timeout: @options.write_timeout) { @engine.enqueue_send(parts) }
4343
self
4444
end

lib/omq/radio_dish.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def initialize(endpoints = nil, linger: Float::INFINITY, on_mute: :drop_newest,
4242
# @return [self]
4343
#
4444
def publish(group, body)
45-
parts = [group.b.freeze, body.b.freeze]
45+
parts = [group, body]
4646
Reactor.run timeout: @options.write_timeout do
4747
@engine.enqueue_send(parts)
4848
end

lib/omq/readable.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ module Readable
1414
# @raise [IO::TimeoutError] if read_timeout exceeded
1515
#
1616
def receive
17-
Reactor.run timeout: @options.read_timeout do |task|
17+
if @engine.on_io_thread?
18+
Reactor.run(timeout: @options.read_timeout) { @engine.dequeue_recv }
19+
elsif (timeout = @options.read_timeout)
20+
Async::Task.current.with_timeout(timeout, IO::TimeoutError) { @engine.dequeue_recv }
21+
else
1822
@engine.dequeue_recv
1923
end
2024
end

lib/omq/transport/inproc/pipe.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ def close
208208

209209
def apply_transform(parts)
210210
if @direct_recv_transform
211-
@direct_recv_transform.call(parts).freeze
211+
@direct_recv_transform.call(parts)
212212
else
213213
parts
214214
end

lib/omq/transport/udp.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ def receive_message
200200
next unless parts
201201
group, body = parts
202202
next unless @groups.include?(group.b)
203-
return [group.b.freeze, body.b.freeze]
203+
return [group, body]
204204
rescue IO::WaitReadable
205205
@socket.wait_readable
206206
retry

lib/omq/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# frozen_string_literal: true
22

33
module OMQ
4-
VERSION = "0.23.1"
4+
VERSION = "0.24.0"
55
end

0 commit comments

Comments
 (0)