omq 0.9.0 → 0.11.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.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +129 -0
  3. data/README.md +28 -3
  4. data/lib/omq/channel.rb +5 -5
  5. data/lib/omq/client_server.rb +10 -10
  6. data/lib/omq/engine.rb +702 -0
  7. data/lib/omq/options.rb +48 -0
  8. data/lib/omq/pair.rb +4 -4
  9. data/lib/omq/peer.rb +5 -5
  10. data/lib/omq/pub_sub.rb +18 -18
  11. data/lib/omq/push_pull.rb +6 -6
  12. data/lib/omq/queue_interface.rb +73 -0
  13. data/lib/omq/radio_dish.rb +6 -6
  14. data/lib/omq/reactor.rb +128 -0
  15. data/lib/omq/readable.rb +44 -0
  16. data/lib/omq/req_rep.rb +8 -8
  17. data/lib/omq/router_dealer.rb +8 -8
  18. data/lib/omq/routing/channel.rb +83 -0
  19. data/lib/omq/routing/client.rb +56 -0
  20. data/lib/omq/routing/dealer.rb +57 -0
  21. data/lib/omq/routing/dish.rb +78 -0
  22. data/lib/omq/routing/fan_out.rb +140 -0
  23. data/lib/omq/routing/gather.rb +46 -0
  24. data/lib/omq/routing/pair.rb +86 -0
  25. data/lib/omq/routing/peer.rb +101 -0
  26. data/lib/omq/routing/pub.rb +60 -0
  27. data/lib/omq/routing/pull.rb +46 -0
  28. data/lib/omq/routing/push.rb +81 -0
  29. data/lib/omq/routing/radio.rb +150 -0
  30. data/lib/omq/routing/rep.rb +101 -0
  31. data/lib/omq/routing/req.rb +65 -0
  32. data/lib/omq/routing/round_robin.rb +168 -0
  33. data/lib/omq/routing/router.rb +110 -0
  34. data/lib/omq/routing/scatter.rb +82 -0
  35. data/lib/omq/routing/server.rb +101 -0
  36. data/lib/omq/routing/sub.rb +78 -0
  37. data/lib/omq/routing/xpub.rb +72 -0
  38. data/lib/omq/routing/xsub.rb +83 -0
  39. data/lib/omq/routing.rb +66 -0
  40. data/lib/omq/scatter_gather.rb +8 -8
  41. data/lib/omq/single_frame.rb +18 -0
  42. data/lib/omq/socket.rb +32 -11
  43. data/lib/omq/transport/inproc.rb +355 -0
  44. data/lib/omq/transport/ipc.rb +117 -0
  45. data/lib/omq/transport/tcp.rb +111 -0
  46. data/lib/omq/transport/tls.rb +146 -0
  47. data/lib/omq/version.rb +1 -1
  48. data/lib/omq/writable.rb +66 -0
  49. data/lib/omq.rb +64 -4
  50. metadata +34 -33
  51. data/lib/omq/zmtp/engine.rb +0 -551
  52. data/lib/omq/zmtp/options.rb +0 -48
  53. data/lib/omq/zmtp/reactor.rb +0 -131
  54. data/lib/omq/zmtp/readable.rb +0 -29
  55. data/lib/omq/zmtp/routing/channel.rb +0 -81
  56. data/lib/omq/zmtp/routing/client.rb +0 -56
  57. data/lib/omq/zmtp/routing/dealer.rb +0 -57
  58. data/lib/omq/zmtp/routing/dish.rb +0 -80
  59. data/lib/omq/zmtp/routing/fan_out.rb +0 -131
  60. data/lib/omq/zmtp/routing/gather.rb +0 -48
  61. data/lib/omq/zmtp/routing/pair.rb +0 -84
  62. data/lib/omq/zmtp/routing/peer.rb +0 -100
  63. data/lib/omq/zmtp/routing/pub.rb +0 -62
  64. data/lib/omq/zmtp/routing/pull.rb +0 -48
  65. data/lib/omq/zmtp/routing/push.rb +0 -80
  66. data/lib/omq/zmtp/routing/radio.rb +0 -139
  67. data/lib/omq/zmtp/routing/rep.rb +0 -101
  68. data/lib/omq/zmtp/routing/req.rb +0 -65
  69. data/lib/omq/zmtp/routing/round_robin.rb +0 -143
  70. data/lib/omq/zmtp/routing/router.rb +0 -109
  71. data/lib/omq/zmtp/routing/scatter.rb +0 -81
  72. data/lib/omq/zmtp/routing/server.rb +0 -100
  73. data/lib/omq/zmtp/routing/sub.rb +0 -80
  74. data/lib/omq/zmtp/routing/xpub.rb +0 -74
  75. data/lib/omq/zmtp/routing/xsub.rb +0 -86
  76. data/lib/omq/zmtp/routing.rb +0 -65
  77. data/lib/omq/zmtp/single_frame.rb +0 -20
  78. data/lib/omq/zmtp/transport/inproc.rb +0 -359
  79. data/lib/omq/zmtp/transport/ipc.rb +0 -118
  80. data/lib/omq/zmtp/transport/tcp.rb +0 -117
  81. data/lib/omq/zmtp/writable.rb +0 -61
  82. data/lib/omq/zmtp.rb +0 -81
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a14af693117ccfb193f54e9bc2a4faae9c2bf6396e7769abee97f279fd62fdba
4
- data.tar.gz: 1f5e99ae3b61402d2b9e506c84fdb07d2003695515d3f736159cf2c373cc39d0
3
+ metadata.gz: b736b6b715f5c4f330d21be278450600b2642239b8bea9e85957f516e89211ed
4
+ data.tar.gz: 34c057a32055e7333e4299e85c0fee9e798b43ea34d021f237ba6778338b65f1
5
5
  SHA512:
6
- metadata.gz: 76539e2a9fbdcbe59590e1fa499e300b8f7ad3d527255af6f94665139c3c0a60151f0b82202df372df1cdbc16e90f46369aa7b159342f951cf52babc897b837c
7
- data.tar.gz: dba241d3283a0a0330ba0d12081c08f3ae074acaa8be7b1e9292dee9c19a483d822f896a4442ec585005dbeb36efa704b87f51c53e07c1de0ede54a85150f55c
6
+ metadata.gz: e95a6df6d4eb56ac1fc0586f61cfdb77d0afe9de620d3e540ffbae1256c2fe6526556485215eda3de20c8f34a332f51a68e5bb075087665049c0219bb07f6960
7
+ data.tar.gz: 8b5c8d38b7523e9c6ab30e17c21b0cf4adb2086e1bdde90298f62e42f4b8f2336cdd95c47293c3fb24fa41f376aed8a2ec6d1121fc56d41aa83d4afdc289d707
data/CHANGELOG.md CHANGED
@@ -1,5 +1,134 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.11.0
4
+
5
+ ### Added
6
+
7
+ - **`backend:` kwarg** — all socket types accept `backend: :ffi` to use
8
+ the libzmq FFI backend (via the [omq-ffi](https://github.com/paddor/omq-ffi)
9
+ gem). Default is `:ruby` (pure Ruby ZMTP). Enables interop testing and
10
+ access to libzmq-specific features without changing the socket API.
11
+ - **TLS transport (`tls+tcp://`)** — TLS v1.3 on top of TCP using Ruby's
12
+ stdlib `openssl`. Set `socket.tls_context` to an `OpenSSL::SSL::SSLContext`
13
+ before bind/connect. Per-socket (not per-endpoint), frozen on first use.
14
+ SNI set automatically from the endpoint hostname. Bad TLS handshakes are
15
+ dropped without killing the accept loop. `OpenSSL::SSL::SSLError` added
16
+ to `CONNECTION_LOST` for automatic reconnection on TLS failures.
17
+ Accompanied by a draft RFC (`rfc/zmtp-tls.md`) defining the transport
18
+ mapping for ZMTP 3.1 over TLS.
19
+ - **PUB/RADIO fan-out pre-encoding** — ZMTP frames are encoded once per
20
+ message and written as raw wire bytes to all non-CURVE subscribers.
21
+ Eliminates redundant `Frame.new` + `#to_wire` calls during fan-out.
22
+ CURVE connections (which encrypt at the ZMTP level) still encode
23
+ per-connection. TLS, NULL, and PLAIN all benefit since TLS encrypts
24
+ below ZMTP. Requires protocol-zmtp `Frame.encode_message` and
25
+ `Connection#write_wire`.
26
+ - **CURVE benchmarks** — all per-pattern benchmarks now include CURVE
27
+ (via rbnacl) alongside inproc, ipc, tcp, and tls transports.
28
+ - **Engine `connection_wrapper` hook** — optional proc on Engine that wraps
29
+ new connections (both inproc and tcp/ipc) at creation time. Used by the
30
+ omq-ractor gem for per-connection serialization (Marshal for tcp/ipc,
31
+ `Ractor.make_shareable` for inproc).
32
+ - **Queue-style interface** — readable sockets gain `#dequeue(timeout:)`,
33
+ `#pop`, `#wait`, and `#each`; writable sockets gain `#enqueue` and
34
+ `#push`. Inspired by `Async::Queue`. `#wait` blocks indefinitely
35
+ (ignores `read_timeout`); `#each` returns gracefully on timeout.
36
+ - **Recv pump fairness** — each connection yields to the fiber scheduler
37
+ after 64 messages or 1 MB (whichever comes first). Prevents a fast or
38
+ large-message connection from starving slower peers when the consumer
39
+ keeps up. Byte counting gracefully handles non-string messages (e.g.
40
+ deserialized objects from connection wrappers).
41
+ - **Per-pattern benchmark suite** — `bench/{push_pull,req_rep,router_dealer,dealer_dealer,pub_sub,pair}/omq.rb`
42
+ with shared helpers (`bench_helper.rb`) and UnicodePlot braille line
43
+ charts (`plot.rb`). Each benchmark measures throughput (msg/s) and
44
+ bandwidth (MB/s) across transports (inproc, ipc, tcp, tls, curve),
45
+ message sizes (64 B–64 KB), and peer counts (1, 3). Plots are written to per-directory
46
+ `README.md` files for easy diffing across versions.
47
+
48
+ ### Changed
49
+
50
+ - **SUB/XSUB `prefix:` kwarg renamed to `subscribe:`** — aligns with
51
+ ZeroMQ conventions. `subscribe: nil` (no subscription) remains the
52
+ default; pass `subscribe: ''` to subscribe to everything, or
53
+ `subscribe: 'topic.'` for a prefix filter.
54
+ - **Scenario benchmarks moved to `bench/scenarios/`** — broker,
55
+ draft_types, flush_batching, hwm_backpressure, large_messages,
56
+ multiframe, pubsub_fanout, ractors_vs_async, ractors_vs_fork,
57
+ reconnect_storm, and reqrep_throughput moved from `bench/` top level.
58
+
59
+ ### Removed
60
+
61
+ - **Old flat benchmarks** — `bench/throughput.rb`, `bench/latency.rb`,
62
+ `bench/pipeline_mbps.rb`, `bench/run_all.sh` replaced by per-pattern
63
+ benchmarks.
64
+ - **`bench/cli/`** — CLI-specific benchmarks (fib pipeline, latency,
65
+ throughput shell scripts) moved to the omq-cli repository.
66
+
67
+ ## 0.10.0 — 2026-04-01
68
+
69
+ ### Added
70
+
71
+ - **Auto-close sockets via Async task tree** — all engine tasks (accept
72
+ loops, connection tasks, send/recv pumps, heartbeats, reconnect loops,
73
+ reapers) now live under the caller's Async task. When the `Async` block
74
+ exits, tasks are stopped and `ensure` blocks close IO resources.
75
+ Explicit `Socket#close` is no longer required (but remains available
76
+ and idempotent).
77
+ - **Non-Async usage** — sockets work outside `Async do…end`. A shared IO
78
+ thread hosts the task tree; all blocking operations (bind, connect,
79
+ send, receive, close) are dispatched to it transparently via
80
+ `Reactor.run`. The IO thread shuts down cleanly at process exit,
81
+ respecting the longest linger across all sockets.
82
+ - **Recv prefetching** — `#receive` internally drains up to 64 messages
83
+ per queue dequeue, buffering the excess behind a Mutex. Subsequent
84
+ calls return from the buffer without touching the queue. Thread-safe
85
+ on JRuby. TCP 64B pipelined: 30k → 221k msg/s (7x).
86
+
87
+ ### Changed
88
+
89
+ - **Transports are pure IO** — TCP and IPC transports no longer spawn
90
+ tasks. They create server sockets and return them; Engine owns the
91
+ accept loops.
92
+ - **Reactor simplified** — `spawn_pump` and `PumpHandle` removed.
93
+ Reactor exposes `root_task` (shared IO thread's root Async task)
94
+ and `run` (cross-thread dispatch). `stop!` respects max linger.
95
+ - **Flatten `OMQ::ZMTP` namespace into `OMQ`** — with the ZMTP protocol
96
+ layer extracted to `protocol-zmtp`, the `ZMTP` sub-namespace no longer
97
+ makes sense. Engine, routing, transport, and mixins now live directly
98
+ under `OMQ::`. Protocol-zmtp types are referenced as `Protocol::ZMTP::*`.
99
+
100
+ ### Performance
101
+
102
+ - **Direct pipe bypass for single-peer inproc** — PAIR, CHANNEL, and
103
+ single-peer RoundRobin types (PUSH, REQ, DEALER, CLIENT, SCATTER)
104
+ enqueue directly into the receiver's recv queue, skipping the
105
+ send_queue and send pump entirely.
106
+ Inproc PUSH/PULL: 200k → 980k msg/s (5x).
107
+ - **Uncapped send queue drain** — the send pump drains the entire queue
108
+ per cycle instead of capping at 64 messages. IO::Stream auto-flushes
109
+ at 64 KB, so writes hit the wire naturally under load.
110
+ IPC latency −12%, TCP latency −10%.
111
+ - **Remove `.b` allocations from PUB/SUB subscription matching** —
112
+ `FanOut#subscribed?` no longer creates temporary binary strings per
113
+ comparison; both topic and prefix are guaranteed binary at rest.
114
+ - **Reuse `written` Set and `latest` Hash across batches** in all send
115
+ pumps (fan-out, round-robin, router, server, peer, rep, radio),
116
+ eliminating per-batch object allocation.
117
+ - **O(1) `connection_removed` for identity-routed sockets** — Router,
118
+ Server, and Peer now maintain a reverse index instead of scanning.
119
+ - **`freeze_message` fast path** — skip `.b.freeze` when the string is
120
+ already a frozen binary string.
121
+ - **Pre-frozen empty frame constants** for REQ/REP delimiter frames.
122
+
123
+ ### Fixed
124
+
125
+ - **Reapers no longer crash on inproc DirectPipe** — PUSH and SCATTER
126
+ reapers skipped for DirectPipe connections that have no receive queue
127
+ (latent bug previously masked by transient task error swallowing).
128
+ - **`send_pump_idle?` made public** on all routing strategies — was
129
+ accidentally private, crashing `Engine#drain_send_queues` with
130
+ linger > 0.
131
+
3
132
  ## 0.9.0 — 2026-03-31
4
133
 
5
134
  ### Breaking
data/README.md CHANGED
@@ -9,9 +9,9 @@
9
9
 
10
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
11
 
12
- > **234k msg/s** inproc | **49k msg/s** ipc | **36k msg/s** tcp
12
+ > **980k msg/s** inproc | **38k msg/s** ipc | **31k msg/s** tcp
13
13
  >
14
- > **12 µs** inproc latency | **51 µs** ipc | **62 µs** tcp
14
+ > **10 µs** inproc latency | **71 µs** ipc | **82 µs** tcp
15
15
  >
16
16
  > Ruby 4.0 + YJIT on a Linux VM — see [`bench/`](bench/) for full results
17
17
 
@@ -30,7 +30,7 @@ See [GETTING_STARTED.md](GETTING_STARTED.md) for a ~30 min walkthrough of all ma
30
30
  ## Highlights
31
31
 
32
32
  - **Zero dependencies on C** — no extensions, no FFI, no libzmq. `gem install` just works everywhere
33
- - **Fast** — YJIT-optimized hot paths, batched sends, 234k msg/s inproc with 12 µs latency
33
+ - **Fast** — YJIT-optimized hot paths, batched sends, recv prefetching, direct-pipe inproc bypass. 980k msg/s inproc, 10 µs latency
34
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
35
35
  - **Every socket pattern** — req/rep, pub/sub, push/pull, dealer/router, xpub/xsub, pair, and all draft types
36
36
  - **Every transport** — tcp, ipc (Unix domain sockets), inproc (in-process queues)
@@ -125,6 +125,26 @@ pull.close
125
125
 
126
126
  The IO thread runs all pumps, reconnection, and heartbeating in the background. When you're inside an `Async` block, OMQ uses the existing reactor instead.
127
127
 
128
+ ### Queue Interface
129
+
130
+ All sockets expose an `Async::Queue`-inspired interface:
131
+
132
+ | Async::Queue | OMQ Socket | Notes |
133
+ |---|---|---|
134
+ | `enqueue(item)` / `push(item)` | `enqueue(msg)` / `push(msg)` | Also: `send(msg)`, `<< msg` |
135
+ | `dequeue(timeout:)` / `pop(timeout:)` | `dequeue(timeout:)` / `pop(timeout:)` | Defaults to socket's `read_timeout` |
136
+ | `wait` | `wait` | Blocks indefinitely (ignores `read_timeout`) |
137
+ | `each` | `each` | Yields messages; returns on close or timeout |
138
+
139
+ ```ruby
140
+ pull = OMQ::PULL.bind('inproc://work')
141
+
142
+ # iterate messages like a queue
143
+ pull.each do |msg|
144
+ puts msg.first
145
+ end
146
+ ```
147
+
128
148
  ## Socket Types
129
149
 
130
150
  All sockets are thread-safe. Default HWM is 1000 messages per socket. Classes live under `OMQ::` (alias: `ØMQ`).
@@ -163,6 +183,11 @@ echo "hello" | omq req -c tcp://localhost:5555
163
183
 
164
184
  See the [omq-cli README](https://github.com/paddor/omq-cli) for full documentation.
165
185
 
186
+ ## Companion Gems
187
+
188
+ - **[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.
189
+ - **[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.
190
+
166
191
  ## Development
167
192
 
168
193
  ```sh
data/lib/omq/channel.rb CHANGED
@@ -2,12 +2,12 @@
2
2
 
3
3
  module OMQ
4
4
  class CHANNEL < Socket
5
- include ZMTP::Readable
6
- include ZMTP::Writable
7
- include ZMTP::SingleFrame
5
+ include Readable
6
+ include Writable
7
+ include SingleFrame
8
8
 
9
- def initialize(endpoints = nil, linger: 0)
10
- _init_engine(:CHANNEL, linger: linger)
9
+ def initialize(endpoints = nil, linger: 0, backend: nil)
10
+ _init_engine(:CHANNEL, linger: linger, backend: backend)
11
11
  _attach(endpoints, default: :connect)
12
12
  end
13
13
  end
@@ -2,23 +2,23 @@
2
2
 
3
3
  module OMQ
4
4
  class CLIENT < Socket
5
- include ZMTP::Readable
6
- include ZMTP::Writable
7
- include ZMTP::SingleFrame
5
+ include Readable
6
+ include Writable
7
+ include SingleFrame
8
8
 
9
- def initialize(endpoints = nil, linger: 0)
10
- _init_engine(:CLIENT, linger: linger)
9
+ def initialize(endpoints = nil, linger: 0, backend: nil)
10
+ _init_engine(:CLIENT, linger: linger, backend: backend)
11
11
  _attach(endpoints, default: :connect)
12
12
  end
13
13
  end
14
14
 
15
15
  class SERVER < Socket
16
- include ZMTP::Readable
17
- include ZMTP::Writable
18
- include ZMTP::SingleFrame
16
+ include Readable
17
+ include Writable
18
+ include SingleFrame
19
19
 
20
- def initialize(endpoints = nil, linger: 0)
21
- _init_engine(:SERVER, linger: linger)
20
+ def initialize(endpoints = nil, linger: 0, backend: nil)
21
+ _init_engine(:SERVER, linger: linger, backend: backend)
22
22
  _attach(endpoints, default: :bind)
23
23
  end
24
24