omq 0.2.0 → 0.2.2
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 +4 -4
- data/CHANGELOG.md +14 -0
- data/README.md +11 -3
- data/lib/omq/version.rb +1 -1
- data/lib/omq/zmtp/codec/command.rb +12 -27
- data/lib/omq/zmtp/codec/frame.rb +12 -27
- data/lib/omq/zmtp/codec/greeting.rb +11 -29
- data/lib/omq.rb +3 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 02d85dfeec36f9d537cdfac7512a4a5edbfbe95145ae9583e8f5428a43a2548d
|
|
4
|
+
data.tar.gz: 1598f1cdecdc0523cf4263a72318f0500b1bd9dccb46da703817fd0b30c74315
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ee523b073066febf851dc2427b8ddd86c62bc44580a3a9c4283a4d72345ee52048e07a396da1db0de7265f7a641ac6aafe02827542a2786833c8e78b0a25bebf
|
|
7
|
+
data.tar.gz: 88933632f2e57c092090af0a322772db4431af55f85f4e9f41c21df2aa88d68bc9641ef3691a6826335b89b8b030275576b978ebf9f558a22ab4a329da5c3bbe
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.2 — 2026-03-26
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `ØMQ` alias for `OMQ` — because Ruby can
|
|
8
|
+
|
|
9
|
+
## 0.2.1 — 2026-03-26
|
|
10
|
+
|
|
11
|
+
### Improved
|
|
12
|
+
|
|
13
|
+
- Replace `IO::Buffer` with `pack`/`unpack1`/`getbyte`/`byteslice` in
|
|
14
|
+
frame, command, and greeting codecs — up to 68% higher throughput for
|
|
15
|
+
large messages, 21% lower TCP latency
|
|
16
|
+
|
|
3
17
|
## 0.2.0 — 2026-03-26
|
|
4
18
|
|
|
5
19
|
### Changed
|
data/README.md
CHANGED
|
@@ -7,7 +7,11 @@
|
|
|
7
7
|
|
|
8
8
|
Pure Ruby implementation of the [ZMTP 3.1](https://rfc.zeromq.org/spec/23/) wire protocol ([ZeroMQ](https://zeromq.org/)) using the [Async](https://github.com/socketry/async) gem. No native libraries required.
|
|
9
9
|
|
|
10
|
-
> **
|
|
10
|
+
> **167k msg/s** inproc | **42k msg/s** ipc | **32k msg/s** tcp
|
|
11
|
+
>
|
|
12
|
+
> **15 µs** inproc latency | **62 µs** ipc | **88 µs** tcp
|
|
13
|
+
>
|
|
14
|
+
> Ruby 4.0 + YJIT on a Linux VM on a 2019 MacBook Pro (Intel)
|
|
11
15
|
|
|
12
16
|
---
|
|
13
17
|
|
|
@@ -25,7 +29,7 @@ Modern Ruby has closed the gap:
|
|
|
25
29
|
|
|
26
30
|
- **YJIT** — JIT-compiled hot paths close the throughput gap with C extensions
|
|
27
31
|
- **Fiber Scheduler** — non-blocking I/O without callbacks or threads (`Async` builds on this)
|
|
28
|
-
- **`
|
|
32
|
+
- **`io-stream`** — buffered I/O with read-ahead, from the Async ecosystem
|
|
29
33
|
|
|
30
34
|
When [CZTop](https://github.com/paddor/cztop) was written, none of this existed. Today, a pure Ruby ZMTP implementation is fast enough for production use — and you get `gem install` with no compiler toolchain, no system packages, and no segfaults.
|
|
31
35
|
|
|
@@ -109,7 +113,11 @@ end
|
|
|
109
113
|
| Routing | `DEALER`, `ROUTER` | bidirectional |
|
|
110
114
|
| Exclusive pair | `PAIR` | bidirectional |
|
|
111
115
|
|
|
112
|
-
All classes live under `OMQ::`.
|
|
116
|
+
All classes live under `OMQ::`. For the purists, `ØMQ` is an alias:
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
req = ØMQ::REQ.new(">tcp://localhost:5555")
|
|
120
|
+
```
|
|
113
121
|
|
|
114
122
|
## Performance
|
|
115
123
|
|
data/lib/omq/version.rb
CHANGED
|
@@ -39,11 +39,7 @@ module OMQ
|
|
|
39
39
|
#
|
|
40
40
|
def to_body
|
|
41
41
|
name_bytes = @name.b
|
|
42
|
-
|
|
43
|
-
buf.set_value(:U8, 0, name_bytes.bytesize)
|
|
44
|
-
buf.set_string(name_bytes, 1)
|
|
45
|
-
buf.set_string(@data, 1 + name_bytes.bytesize)
|
|
46
|
-
buf.get_string(0, buf.size, Encoding::BINARY)
|
|
42
|
+
name_bytes.bytesize.chr.b + name_bytes + @data
|
|
47
43
|
end
|
|
48
44
|
|
|
49
45
|
# Encodes as a complete command Frame.
|
|
@@ -64,12 +60,11 @@ module OMQ
|
|
|
64
60
|
body = body.b
|
|
65
61
|
raise ProtocolError, "command body too short" if body.bytesize < 1
|
|
66
62
|
|
|
67
|
-
|
|
68
|
-
name_len = buf.get_value(:U8, 0)
|
|
63
|
+
name_len = body.getbyte(0)
|
|
69
64
|
|
|
70
65
|
raise ProtocolError, "command name truncated" if body.bytesize < 1 + name_len
|
|
71
66
|
|
|
72
|
-
name =
|
|
67
|
+
name = body.byteslice(1, name_len)
|
|
73
68
|
data = body.byteslice(1 + name_len..)
|
|
74
69
|
new(name, data)
|
|
75
70
|
end
|
|
@@ -113,12 +108,8 @@ module OMQ
|
|
|
113
108
|
# @return [Command]
|
|
114
109
|
#
|
|
115
110
|
def self.ping(ttl: 0, context: "".b)
|
|
116
|
-
# TTL is encoded as 2-byte big-endian value in tenths of a second
|
|
117
111
|
ttl_ds = (ttl * 10).to_i
|
|
118
|
-
|
|
119
|
-
buf.set_value(:U16, 0, ttl_ds)
|
|
120
|
-
buf.set_string(context.b, 2) if context.bytesize > 0
|
|
121
|
-
new("PING", buf.get_string(0, buf.size, Encoding::BINARY))
|
|
112
|
+
new("PING", [ttl_ds].pack("n") + context.b)
|
|
122
113
|
end
|
|
123
114
|
|
|
124
115
|
# Builds a PONG command.
|
|
@@ -135,8 +126,7 @@ module OMQ
|
|
|
135
126
|
# @return [Array(Numeric, String)] [ttl_seconds, context_bytes]
|
|
136
127
|
#
|
|
137
128
|
def ping_ttl_and_context
|
|
138
|
-
|
|
139
|
-
ttl_ds = buf.get_value(:U16, 0)
|
|
129
|
+
ttl_ds = @data.unpack1("n")
|
|
140
130
|
context = @data.bytesize > 2 ? @data.byteslice(2..) : "".b
|
|
141
131
|
[ttl_ds / 10.0, context]
|
|
142
132
|
end
|
|
@@ -157,17 +147,13 @@ module OMQ
|
|
|
157
147
|
#
|
|
158
148
|
def self.encode_properties(props)
|
|
159
149
|
parts = props.map do |name, value|
|
|
160
|
-
name_bytes
|
|
150
|
+
name_bytes = name.b
|
|
161
151
|
value_bytes = value.b
|
|
162
|
-
|
|
163
|
-
buf.set_value(:U8, 0, name_bytes.bytesize)
|
|
164
|
-
buf.set_string(name_bytes, 1)
|
|
165
|
-
buf.set_value(:U32, 1 + name_bytes.bytesize, value_bytes.bytesize) # big-endian
|
|
166
|
-
buf.set_string(value_bytes, 1 + name_bytes.bytesize + 4)
|
|
167
|
-
buf.get_string(0, buf.size, Encoding::BINARY)
|
|
152
|
+
name_bytes.bytesize.chr.b + name_bytes + [value_bytes.bytesize].pack("N") + value_bytes
|
|
168
153
|
end
|
|
169
154
|
parts.join
|
|
170
155
|
end
|
|
156
|
+
|
|
171
157
|
# Decodes a ZMTP property list from binary data.
|
|
172
158
|
#
|
|
173
159
|
# @param data [String] binary property list
|
|
@@ -176,24 +162,23 @@ module OMQ
|
|
|
176
162
|
#
|
|
177
163
|
def self.decode_properties(data)
|
|
178
164
|
result = {}
|
|
179
|
-
buf = IO::Buffer.for(data)
|
|
180
165
|
offset = 0
|
|
181
166
|
|
|
182
167
|
while offset < data.bytesize
|
|
183
168
|
raise ProtocolError, "property name truncated" if offset + 1 > data.bytesize
|
|
184
|
-
name_len =
|
|
169
|
+
name_len = data.getbyte(offset)
|
|
185
170
|
offset += 1
|
|
186
171
|
|
|
187
172
|
raise ProtocolError, "property name truncated" if offset + name_len > data.bytesize
|
|
188
|
-
name =
|
|
173
|
+
name = data.byteslice(offset, name_len)
|
|
189
174
|
offset += name_len
|
|
190
175
|
|
|
191
176
|
raise ProtocolError, "property value length truncated" if offset + 4 > data.bytesize
|
|
192
|
-
value_len =
|
|
177
|
+
value_len = data.byteslice(offset, 4).unpack1("N")
|
|
193
178
|
offset += 4
|
|
194
179
|
|
|
195
180
|
raise ProtocolError, "property value truncated" if offset + value_len > data.bytesize
|
|
196
|
-
value =
|
|
181
|
+
value = data.byteslice(offset, value_len)
|
|
197
182
|
offset += value_len
|
|
198
183
|
|
|
199
184
|
result[name] = value
|
data/lib/omq/zmtp/codec/frame.rb
CHANGED
|
@@ -46,52 +46,37 @@ module OMQ
|
|
|
46
46
|
# @return [String] binary wire representation (flags + size + body)
|
|
47
47
|
#
|
|
48
48
|
def to_wire
|
|
49
|
-
size
|
|
49
|
+
size = @body.bytesize
|
|
50
50
|
flags = 0
|
|
51
51
|
flags |= FLAGS_MORE if @more
|
|
52
52
|
flags |= FLAGS_COMMAND if @command
|
|
53
53
|
|
|
54
54
|
if size > SHORT_MAX
|
|
55
|
-
flags
|
|
56
|
-
buf = IO::Buffer.new(9 + size)
|
|
57
|
-
buf.set_value(:U8, 0, flags)
|
|
58
|
-
buf.set_value(:U64, 1, size) # big-endian
|
|
59
|
-
buf.set_string(@body, 9)
|
|
60
|
-
buf.get_string(0, 9 + size, Encoding::BINARY)
|
|
55
|
+
(flags | FLAGS_LONG).chr.b + [size].pack("Q>") + @body
|
|
61
56
|
else
|
|
62
|
-
|
|
63
|
-
buf.set_value(:U8, 0, flags)
|
|
64
|
-
buf.set_value(:U8, 1, size)
|
|
65
|
-
buf.set_string(@body, 2)
|
|
66
|
-
buf.get_string(0, 2 + size, Encoding::BINARY)
|
|
57
|
+
flags.chr.b + size.chr.b + @body
|
|
67
58
|
end
|
|
68
59
|
end
|
|
69
60
|
|
|
70
61
|
# Reads one frame from an IO-like object.
|
|
71
62
|
#
|
|
72
|
-
# @param io [#
|
|
63
|
+
# @param io [#read_exactly] must support read_exactly(n)
|
|
73
64
|
# @return [Frame]
|
|
74
65
|
# @raise [ProtocolError] on invalid frame
|
|
75
66
|
# @raise [EOFError] if the connection is closed
|
|
76
67
|
#
|
|
77
68
|
def self.read_from(io)
|
|
78
|
-
|
|
79
|
-
flags_buf = IO::Buffer.for(flags_byte)
|
|
80
|
-
flags = flags_buf.get_value(:U8, 0)
|
|
69
|
+
flags = io.read_exactly(1).getbyte(0)
|
|
81
70
|
|
|
82
|
-
more
|
|
83
|
-
long
|
|
71
|
+
more = (flags & FLAGS_MORE) != 0
|
|
72
|
+
long = (flags & FLAGS_LONG) != 0
|
|
84
73
|
command = (flags & FLAGS_COMMAND) != 0
|
|
85
74
|
|
|
86
|
-
if long
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
size_byte = io.read_exactly(1)
|
|
92
|
-
size_buf = IO::Buffer.for(size_byte)
|
|
93
|
-
size = size_buf.get_value(:U8, 0)
|
|
94
|
-
end
|
|
75
|
+
size = if long
|
|
76
|
+
io.read_exactly(8).unpack1("Q>")
|
|
77
|
+
else
|
|
78
|
+
io.read_exactly(1).getbyte(0)
|
|
79
|
+
end
|
|
95
80
|
|
|
96
81
|
body = size > 0 ? io.read_exactly(size) : "".b
|
|
97
82
|
|
|
@@ -33,26 +33,11 @@ module OMQ
|
|
|
33
33
|
# @return [String] 64-byte binary greeting
|
|
34
34
|
#
|
|
35
35
|
def self.encode(mechanism: "NULL", as_server: false)
|
|
36
|
-
buf =
|
|
37
|
-
buf.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
buf
|
|
41
|
-
# bytes 1-8 are already 0x00
|
|
42
|
-
buf.set_value(:U8, 9, SIGNATURE_END)
|
|
43
|
-
|
|
44
|
-
# Version
|
|
45
|
-
buf.set_value(:U8, 10, VERSION_MAJOR)
|
|
46
|
-
buf.set_value(:U8, 11, VERSION_MINOR)
|
|
47
|
-
|
|
48
|
-
# Mechanism (null-padded)
|
|
49
|
-
buf.set_string(mechanism.b, MECHANISM_OFFSET)
|
|
50
|
-
|
|
51
|
-
# As-server flag
|
|
52
|
-
buf.set_value(:U8, AS_SERVER_OFFSET, as_server ? 1 : 0)
|
|
53
|
-
|
|
54
|
-
# Filler bytes 33-63 are already 0x00
|
|
55
|
-
buf.get_string(0, SIZE, Encoding::BINARY)
|
|
36
|
+
buf = "\xFF".b + ("\x00" * 8) + "\x7F".b
|
|
37
|
+
buf << [VERSION_MAJOR, VERSION_MINOR].pack("CC")
|
|
38
|
+
buf << mechanism.b.ljust(MECHANISM_LENGTH, "\x00")
|
|
39
|
+
buf << (as_server ? "\x01" : "\x00")
|
|
40
|
+
buf << ("\x00" * 31)
|
|
56
41
|
end
|
|
57
42
|
|
|
58
43
|
# Decodes a ZMTP greeting.
|
|
@@ -64,24 +49,21 @@ module OMQ
|
|
|
64
49
|
def self.decode(data)
|
|
65
50
|
raise ProtocolError, "greeting too short (#{data.bytesize} bytes)" if data.bytesize < SIZE
|
|
66
51
|
|
|
67
|
-
|
|
52
|
+
data = data.b
|
|
68
53
|
|
|
69
|
-
|
|
70
|
-
unless buf.get_value(:U8, 0) == SIGNATURE_START &&
|
|
71
|
-
buf.get_value(:U8, 9) == SIGNATURE_END
|
|
54
|
+
unless data.getbyte(0) == SIGNATURE_START && data.getbyte(9) == SIGNATURE_END
|
|
72
55
|
raise ProtocolError, "invalid greeting signature"
|
|
73
56
|
end
|
|
74
57
|
|
|
75
|
-
major =
|
|
76
|
-
minor =
|
|
58
|
+
major = data.getbyte(10)
|
|
59
|
+
minor = data.getbyte(11)
|
|
77
60
|
|
|
78
61
|
unless major >= 3
|
|
79
62
|
raise ProtocolError, "unsupported ZMTP version #{major}.#{minor} (need >= 3.0)"
|
|
80
63
|
end
|
|
81
64
|
|
|
82
|
-
mechanism =
|
|
83
|
-
|
|
84
|
-
as_server = buf.get_value(:U8, AS_SERVER_OFFSET) == 1
|
|
65
|
+
mechanism = data.byteslice(MECHANISM_OFFSET, MECHANISM_LENGTH).delete("\x00")
|
|
66
|
+
as_server = data.getbyte(AS_SERVER_OFFSET) == 1
|
|
85
67
|
|
|
86
68
|
{
|
|
87
69
|
major: major,
|
data/lib/omq.rb
CHANGED