omq 0.28.10 → 0.29.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 +4 -4
- data/CHANGELOG.md +21 -0
- data/README.md +0 -10
- data/lib/omq/engine/connection_lifecycle.rb +8 -6
- data/lib/omq/engine/reconnect.rb +2 -0
- data/lib/omq/engine/recv_pump.rb +2 -3
- data/lib/omq/options.rb +1 -5
- data/lib/omq/routing/rep.rb +38 -22
- data/lib/omq/routing/req.rb +42 -8
- data/lib/omq/transport/compressed_connection.rb +66 -0
- data/lib/omq/transport/compression_arena.rb +121 -0
- data/lib/omq/transport/inproc/pipe.rb +18 -4
- data/lib/omq/transport/inproc.rb +4 -19
- data/lib/omq/version.rb +1 -1
- metadata +10 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0c36b1596063dc355a2c30ad2ee00d5b5646574fbd39aaf7185334f9d2edeacf
|
|
4
|
+
data.tar.gz: 9fec0824a06493d620b59c808f59864ea43e7f57c0e1a86ef7d0bbcb5a3c0c68
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dcc0b56ab0ee65f8e537ecefb3599802f25053bd70ee25acc307a02e34124eafe50ca651010565ec15c3e9b5b8032da480ef458f35b94bd93e3114c444ab435a
|
|
7
|
+
data.tar.gz: fd213cabc8e6cb194640a474ce1fd4d255a8c959b9110ced374c17c8d2a983f4ed1fd76b0bdb0b5fcb964eb7b654b58c3f6437ac039b0af6e382ebbf7b7bddf3
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.29.0] - 2026-10-15
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Shared bounded arena encoding and decoding for the LZ4 and Zstd transports.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Stop reconnecting with the same credentials after a fatal handshake ERROR.
|
|
14
|
+
- Enforced REQ/REP send/receive order and rejected incomplete routing envelopes.
|
|
15
|
+
- Matched REQ replies to the selected peer and kept REP reply destinations with
|
|
16
|
+
their requests across disconnects. Replies to muted REP peers are discarded.
|
|
17
|
+
- Applied in-process receive transforms to buffered messages and discarded
|
|
18
|
+
filtered messages without delivering close sentinels.
|
|
19
|
+
- Removed in-process peer connections when the opposite pipe closes, including
|
|
20
|
+
connections that bypass the receive pump.
|
|
21
|
+
|
|
22
|
+
### Removed
|
|
23
|
+
|
|
24
|
+
- Removed `omq-qos`, its socket options, and its test and release tasks.
|
|
25
|
+
|
|
5
26
|
## [0.28.10] - 2026-08-26
|
|
6
27
|
|
|
7
28
|
### Changed
|
data/README.md
CHANGED
|
@@ -278,16 +278,6 @@ post-handshake message parts are compressed.
|
|
|
278
278
|
`omq-lz4` for new compressed TCP work. The Rust backend also exposes
|
|
279
279
|
`zstd+tcp://`.
|
|
280
280
|
|
|
281
|
-
### Protocol extensions (RFCs)
|
|
282
|
-
|
|
283
|
-
Optional plug-ins that extend the ZMTP wire protocol. Each is a separate gem.
|
|
284
|
-
load the ones you need.
|
|
285
|
-
|
|
286
|
-
- **[omq-qos](gems/omq-qos)**: experimental MQTT-style at-least-once
|
|
287
|
-
delivery. Receivers send an ACK command frame keyed by xxHash digest.
|
|
288
|
-
unacked messages re-enqueue to the next peer on disconnect. PUSH/PULL,
|
|
289
|
-
SCATTER/GATHER, REQ/REP only. Fan-out patterns are out of scope.
|
|
290
|
-
|
|
291
281
|
## Development
|
|
292
282
|
|
|
293
283
|
```sh
|
|
@@ -117,11 +117,12 @@ module OMQ
|
|
|
117
117
|
|
|
118
118
|
conn&.close
|
|
119
119
|
|
|
120
|
-
#
|
|
121
|
-
#
|
|
120
|
+
# Reconnect after temporary failures. An explicit ERROR response
|
|
121
|
+
# forbids retrying with the same credentials. Without tear-down,
|
|
122
|
+
# spawn_connection's ensure-block close! skips maybe_reconnect,
|
|
122
123
|
# leaving the endpoint dead. Race is exposed when a peer RSTs
|
|
123
124
|
# mid-handshake (e.g. LINGER 0 close against an in-flight connect).
|
|
124
|
-
tear_down!(reconnect:
|
|
125
|
+
tear_down!(reconnect: !error.is_a?(Protocol::ZMTP::FatalHandshakeError))
|
|
125
126
|
raise
|
|
126
127
|
end
|
|
127
128
|
|
|
@@ -132,6 +133,9 @@ module OMQ
|
|
|
132
133
|
# @param pipe [Transport::Inproc::Pipe]
|
|
133
134
|
#
|
|
134
135
|
def ready_direct!(pipe)
|
|
136
|
+
if pipe.is_a?(Transport::Inproc::Pipe) && pipe.peer
|
|
137
|
+
@barrier.async(transient: true, annotation: "inproc peer close") { pipe.peer.wait_closed }
|
|
138
|
+
end
|
|
135
139
|
ready!(pipe)
|
|
136
140
|
end
|
|
137
141
|
|
|
@@ -186,9 +190,7 @@ module OMQ
|
|
|
186
190
|
@engine.peer_connected.resolve(@conn)
|
|
187
191
|
transition!(:ready)
|
|
188
192
|
|
|
189
|
-
#
|
|
190
|
-
# wire the recv/send paths synchronously (no task-based pumps),
|
|
191
|
-
# and isolated unit tests use a FakeEngine without pumps at all.
|
|
193
|
+
# Isolated unit tests can use a FakeEngine without pumps at all.
|
|
192
194
|
# Waiting on an empty barrier returns immediately and would
|
|
193
195
|
# tear the connection down right after registering.
|
|
194
196
|
start_supervisor unless @barrier.empty?
|
data/lib/omq/engine/reconnect.rb
CHANGED
|
@@ -60,6 +60,8 @@ module OMQ
|
|
|
60
60
|
begin
|
|
61
61
|
@dialer.connect
|
|
62
62
|
break
|
|
63
|
+
rescue Protocol::ZMTP::FatalHandshakeError
|
|
64
|
+
break
|
|
63
65
|
rescue *CONNECTION_LOST, *CONNECTION_FAILED, Protocol::ZMTP::Error
|
|
64
66
|
delay = next_delay(delay, max_delay)
|
|
65
67
|
@engine.emit_monitor_event :connect_retried,
|
data/lib/omq/engine/recv_pump.rb
CHANGED
|
@@ -48,7 +48,7 @@ module OMQ
|
|
|
48
48
|
@conn = conn
|
|
49
49
|
@recv_queue = recv_queue
|
|
50
50
|
@engine = engine
|
|
51
|
-
@count_bytes = conn.instance_of?(Protocol::ZMTP::Connection)
|
|
51
|
+
@count_bytes = conn.instance_of?(Protocol::ZMTP::Connection) || conn.respond_to?(:last_wire_size_in)
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
|
|
@@ -83,8 +83,7 @@ module OMQ
|
|
|
83
83
|
# A transform that returns +nil+ drops the message — the recv pump
|
|
84
84
|
# still counts it toward fairness (so dup-floods can't starve
|
|
85
85
|
# siblings) but it is neither emitted nor enqueued to the
|
|
86
|
-
# application.
|
|
87
|
-
# the transform ACKs the sender and returns nil.
|
|
86
|
+
# application.
|
|
88
87
|
#
|
|
89
88
|
# @param parent [Async::Task, Async::Barrier]
|
|
90
89
|
# @param transform [Proc]
|
data/lib/omq/options.rb
CHANGED
|
@@ -32,7 +32,6 @@ module OMQ
|
|
|
32
32
|
@rcvbuf = nil # bytes, nil = OS default
|
|
33
33
|
@on_mute = :block # :block, :drop_newest, :drop_oldest
|
|
34
34
|
@mechanism = Protocol::ZMTP::Mechanism::Null.new
|
|
35
|
-
@qos = 0 # 0 = fire-and-forget, 1 = at-least-once (see omq-qos gem)
|
|
36
35
|
end
|
|
37
36
|
|
|
38
37
|
|
|
@@ -71,8 +70,6 @@ module OMQ
|
|
|
71
70
|
# @return [Symbol] mute strategy (:block, :drop_newest, :drop_oldest)
|
|
72
71
|
# @!attribute mechanism
|
|
73
72
|
# @return [Protocol::ZMTP::Mechanism::Null, Protocol::ZMTP::Mechanism::Curve] security mechanism
|
|
74
|
-
# @!attribute qos
|
|
75
|
-
# @return [Integer] quality of service level (0 = fire-and-forget)
|
|
76
73
|
#
|
|
77
74
|
attr_accessor :send_hwm, :recv_hwm,
|
|
78
75
|
:linger, :identity,
|
|
@@ -83,8 +80,7 @@ module OMQ
|
|
|
83
80
|
:max_message_size,
|
|
84
81
|
:sndbuf, :rcvbuf,
|
|
85
82
|
:on_mute,
|
|
86
|
-
:mechanism
|
|
87
|
-
:qos
|
|
83
|
+
:mechanism
|
|
88
84
|
|
|
89
85
|
# @return [Boolean] true if router_mandatory is set
|
|
90
86
|
alias_method :router_mandatory?, :router_mandatory
|
data/lib/omq/routing/rep.rb
CHANGED
|
@@ -20,10 +20,11 @@ module OMQ
|
|
|
20
20
|
# @param engine [Engine]
|
|
21
21
|
#
|
|
22
22
|
def initialize(engine)
|
|
23
|
-
@engine
|
|
24
|
-
@recv_queue
|
|
25
|
-
@
|
|
26
|
-
@
|
|
23
|
+
@engine = engine
|
|
24
|
+
@recv_queue = Routing.build_queue(engine.options.recv_hwm, :block)
|
|
25
|
+
@current_reply = nil
|
|
26
|
+
@receiving = false
|
|
27
|
+
@conn_queues = {}
|
|
27
28
|
end
|
|
28
29
|
|
|
29
30
|
|
|
@@ -32,7 +33,19 @@ module OMQ
|
|
|
32
33
|
# @return [Array<String>, nil]
|
|
33
34
|
#
|
|
34
35
|
def dequeue_recv
|
|
35
|
-
@
|
|
36
|
+
raise SocketError, "REP socket expects recv/send/recv/send order" if @current_reply || @receiving
|
|
37
|
+
|
|
38
|
+
@receiving = true
|
|
39
|
+
begin
|
|
40
|
+
request = @recv_queue.dequeue
|
|
41
|
+
return unless request
|
|
42
|
+
|
|
43
|
+
connection, envelope, body = request
|
|
44
|
+
@current_reply = [connection, envelope]
|
|
45
|
+
body
|
|
46
|
+
ensure
|
|
47
|
+
@receiving = false
|
|
48
|
+
end
|
|
36
49
|
end
|
|
37
50
|
|
|
38
51
|
|
|
@@ -48,17 +61,23 @@ module OMQ
|
|
|
48
61
|
# @param connection [Protocol::ZMTP::Connection]
|
|
49
62
|
#
|
|
50
63
|
def connection_added(connection)
|
|
64
|
+
q = Routing.build_queue(@engine.options.send_hwm, :drop_newest)
|
|
65
|
+
@conn_queues[connection] = q
|
|
66
|
+
|
|
51
67
|
@engine.start_recv_pump(connection, @recv_queue) do |msg|
|
|
52
|
-
delimiter = msg.index { |p| p.empty? }
|
|
53
|
-
|
|
54
|
-
body = msg[(delimiter + 1)..] || []
|
|
68
|
+
delimiter = msg.index { |p| p.empty? }
|
|
69
|
+
next unless delimiter && delimiter < msg.size - 1
|
|
55
70
|
|
|
56
|
-
|
|
57
|
-
body
|
|
71
|
+
envelope = msg[0, delimiter].freeze
|
|
72
|
+
body = msg[(delimiter + 1)..].freeze
|
|
73
|
+
|
|
74
|
+
# Keep the routing information with its body until the application
|
|
75
|
+
# receives it, even if the peer disconnects in the meantime.
|
|
76
|
+
@engine.emit_verbose_msg_received(connection, body)
|
|
77
|
+
@recv_queue.enqueue([connection, envelope, body].freeze)
|
|
78
|
+
nil
|
|
58
79
|
end
|
|
59
80
|
|
|
60
|
-
q = Routing.build_queue(@engine.options.send_hwm, :block)
|
|
61
|
-
@conn_queues[connection] = q
|
|
62
81
|
ConnSendPump.start(@engine, connection, q)
|
|
63
82
|
end
|
|
64
83
|
|
|
@@ -66,25 +85,22 @@ module OMQ
|
|
|
66
85
|
# @param connection [Protocol::ZMTP::Connection]
|
|
67
86
|
#
|
|
68
87
|
def connection_removed(connection)
|
|
69
|
-
@pending_replies.reject! { |r| r[0] == connection }
|
|
70
88
|
@conn_queues.delete(connection)
|
|
71
89
|
end
|
|
72
90
|
|
|
73
91
|
|
|
74
92
|
# Enqueues a reply. Routes to the connection that sent the matching
|
|
75
|
-
# request by
|
|
93
|
+
# request most recently received by the application. Replies to
|
|
94
|
+
# disconnected or muted peers are discarded.
|
|
76
95
|
#
|
|
77
96
|
# @param parts [Array<String>]
|
|
78
97
|
#
|
|
79
98
|
def enqueue(parts)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
conn,
|
|
84
|
-
|
|
85
|
-
msg << EMPTY_FRAME
|
|
86
|
-
msg.concat(parts)
|
|
87
|
-
@conn_queues[conn]&.enqueue(msg)
|
|
99
|
+
raise SocketError, "REP socket expects recv/send/recv/send order" unless @current_reply
|
|
100
|
+
|
|
101
|
+
conn, envelope = @current_reply
|
|
102
|
+
@conn_queues[conn]&.enqueue([*envelope, EMPTY_FRAME, *parts].freeze)
|
|
103
|
+
@current_reply = nil
|
|
88
104
|
end
|
|
89
105
|
|
|
90
106
|
|
data/lib/omq/routing/req.rb
CHANGED
|
@@ -21,9 +21,11 @@ module OMQ
|
|
|
21
21
|
# @param engine [Engine]
|
|
22
22
|
#
|
|
23
23
|
def initialize(engine)
|
|
24
|
-
@engine
|
|
25
|
-
@recv_queue
|
|
26
|
-
@state
|
|
24
|
+
@engine = engine
|
|
25
|
+
@recv_queue = Routing.build_queue(engine.options.recv_hwm, :block)
|
|
26
|
+
@state = :ready
|
|
27
|
+
@reply_connection = nil
|
|
28
|
+
@receiving = false
|
|
27
29
|
init_round_robin(engine)
|
|
28
30
|
end
|
|
29
31
|
|
|
@@ -33,7 +35,19 @@ module OMQ
|
|
|
33
35
|
# @return [Array<String>, nil]
|
|
34
36
|
#
|
|
35
37
|
def dequeue_recv
|
|
36
|
-
@
|
|
38
|
+
raise SocketError, "REQ socket expects send/recv/send/recv order" if @state == :ready || @receiving
|
|
39
|
+
|
|
40
|
+
@receiving = true
|
|
41
|
+
begin
|
|
42
|
+
msg = @recv_queue.dequeue
|
|
43
|
+
if msg
|
|
44
|
+
@state = :ready
|
|
45
|
+
@reply_connection = nil
|
|
46
|
+
end
|
|
47
|
+
msg
|
|
48
|
+
ensure
|
|
49
|
+
@receiving = false
|
|
50
|
+
end
|
|
37
51
|
end
|
|
38
52
|
|
|
39
53
|
|
|
@@ -50,8 +64,11 @@ module OMQ
|
|
|
50
64
|
#
|
|
51
65
|
def connection_added(connection)
|
|
52
66
|
@engine.start_recv_pump(connection, @recv_queue) do |msg|
|
|
53
|
-
@state
|
|
54
|
-
msg.
|
|
67
|
+
next unless @state == :waiting_reply && connection.equal?(@reply_connection)
|
|
68
|
+
next unless msg.size >= 2 && msg.first.empty?
|
|
69
|
+
|
|
70
|
+
@state = :reply_queued
|
|
71
|
+
msg[1..].freeze
|
|
55
72
|
end
|
|
56
73
|
|
|
57
74
|
add_round_robin_send_connection(connection)
|
|
@@ -71,17 +88,34 @@ module OMQ
|
|
|
71
88
|
def enqueue(parts)
|
|
72
89
|
raise SocketError, "REQ socket expects send/recv/send/recv order" unless @state == :ready
|
|
73
90
|
@state = :waiting_reply
|
|
74
|
-
|
|
91
|
+
accepted = false
|
|
92
|
+
begin
|
|
93
|
+
@reply_connection = @direct_pipe if @direct_pipe&.direct_recv_queue
|
|
94
|
+
enqueue_round_robin(parts)
|
|
95
|
+
accepted = true
|
|
96
|
+
ensure
|
|
97
|
+
unless accepted
|
|
98
|
+
@state = :ready
|
|
99
|
+
@reply_connection = nil
|
|
100
|
+
end
|
|
101
|
+
end
|
|
75
102
|
end
|
|
76
103
|
|
|
77
104
|
|
|
78
105
|
private
|
|
79
106
|
|
|
80
107
|
|
|
108
|
+
# The send pump selects the peer when it takes the request off the queue.
|
|
109
|
+
def write_batch(connection, batch)
|
|
110
|
+
@reply_connection = connection
|
|
111
|
+
super
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
|
|
81
115
|
# REQ prepends empty delimiter frame on the wire.
|
|
82
116
|
#
|
|
83
117
|
def transform_send(parts)
|
|
84
|
-
parts.dup.unshift(EMPTY_BINARY)
|
|
118
|
+
parts.dup.unshift(EMPTY_BINARY).freeze
|
|
85
119
|
end
|
|
86
120
|
|
|
87
121
|
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "delegate"
|
|
4
|
+
require_relative "compression_arena"
|
|
5
|
+
|
|
6
|
+
module OMQ
|
|
7
|
+
module Transport
|
|
8
|
+
class CompressedConnection < SimpleDelegator
|
|
9
|
+
attr_reader :last_wire_size_in
|
|
10
|
+
|
|
11
|
+
def initialize(conn, max_message_size:, arena_sentinel:, protocol_error:)
|
|
12
|
+
super(conn)
|
|
13
|
+
@max_message_size = max_message_size
|
|
14
|
+
@arena_sentinel = arena_sentinel
|
|
15
|
+
@protocol_error = protocol_error
|
|
16
|
+
@pending_messages = []
|
|
17
|
+
@last_wire_size_in = nil
|
|
18
|
+
|
|
19
|
+
# The outer framer sees transport envelopes, not logical messages.
|
|
20
|
+
# Also allow LZ4's worst-case block overhead and dictionary shipment.
|
|
21
|
+
if max_message_size && conn.respond_to?(:max_message_size=)
|
|
22
|
+
overhead = max_message_size / 255 + 32 + 20 * (max_message_size / 1_073_741_824 + 1)
|
|
23
|
+
conn.max_message_size = [max_message_size + overhead, CompressionArena::MAX_WIRE_SIZE].max
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def receive_message(&block)
|
|
29
|
+
unless @pending_messages.empty?
|
|
30
|
+
@last_wire_size_in = 0
|
|
31
|
+
return @pending_messages.shift
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
loop do
|
|
35
|
+
parts = __getobj__.receive_message(&block)
|
|
36
|
+
if parts.any? { |part| part.start_with?(@arena_sentinel) }
|
|
37
|
+
raise @protocol_error, "arena must be a single-part message" unless parts.size == 1
|
|
38
|
+
|
|
39
|
+
@pending_messages = CompressionArena.decode(
|
|
40
|
+
parts.first, max_message_size: @max_message_size, error: @protocol_error,
|
|
41
|
+
) { |data, size| decompress_arena(data, size) }
|
|
42
|
+
@last_wire_size_in = parts.first.bytesize
|
|
43
|
+
return @pending_messages.shift
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
decoded = decode_parts(parts)
|
|
47
|
+
next unless decoded
|
|
48
|
+
|
|
49
|
+
@last_wire_size_in = parts.sum(&:bytesize)
|
|
50
|
+
return decoded
|
|
51
|
+
end
|
|
52
|
+
rescue Protocol::ZMTP::Error
|
|
53
|
+
close
|
|
54
|
+
raise
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# Fan-out must pass logical messages through the compression layer.
|
|
59
|
+
def respond_to?(name, include_private = false)
|
|
60
|
+
return false if [:write_wire, :write_wire_batch].include?(name)
|
|
61
|
+
|
|
62
|
+
super
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module Transport
|
|
5
|
+
# Shared revision 2 envelope and inner ZMTP framing for LZ4 and Zstd.
|
|
6
|
+
module CompressionArena
|
|
7
|
+
HEADER_SIZE = 16
|
|
8
|
+
MAX_BYTES = 65_536
|
|
9
|
+
MAX_MESSAGES = 256
|
|
10
|
+
MAX_PARTS = 4096
|
|
11
|
+
MAX_WIRE_SIZE = HEADER_SIZE + MAX_BYTES
|
|
12
|
+
|
|
13
|
+
# Account for the decoded String and its entry in the parts array.
|
|
14
|
+
PART_CHARGE = 64
|
|
15
|
+
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
# Split only messages already admitted by routing. Never wait to fill
|
|
19
|
+
# an arena, and leave isolated or oversized messages in per-part form.
|
|
20
|
+
def each_batch(messages, target:)
|
|
21
|
+
return enum_for(__method__, messages, target: target) unless block_given?
|
|
22
|
+
|
|
23
|
+
batch = []
|
|
24
|
+
bytes = 0
|
|
25
|
+
parts = 0
|
|
26
|
+
messages.each do |message|
|
|
27
|
+
size = message.sum { |part| part.bytesize + (part.bytesize > 255 ? 9 : 2) }
|
|
28
|
+
eligible = !message.empty? && size <= target && message.size <= MAX_PARTS
|
|
29
|
+
if !batch.empty? && (!eligible || bytes + size > target ||
|
|
30
|
+
parts + message.size > MAX_PARTS || batch.size == MAX_MESSAGES)
|
|
31
|
+
yield batch
|
|
32
|
+
batch = []
|
|
33
|
+
bytes = 0
|
|
34
|
+
parts = 0
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
if eligible
|
|
38
|
+
batch << message
|
|
39
|
+
bytes += size
|
|
40
|
+
parts += message.size
|
|
41
|
+
else
|
|
42
|
+
yield [message]
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
yield batch unless batch.empty?
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def encode(messages, sentinel:, threshold:)
|
|
50
|
+
data = messages.map { |parts| Protocol::ZMTP::Codec::Frame.encode_message(parts) }.join.b
|
|
51
|
+
compressed = yield(data) if data.bytesize >= threshold
|
|
52
|
+
mode = compressed && compressed.bytesize < data.bytesize ? 1 : 0
|
|
53
|
+
sentinel + [mode, "\0\0\0", data.bytesize, messages.size].pack("Ca3VV") + (mode == 1 ? compressed : data)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def decode(wire, max_message_size:, error:)
|
|
58
|
+
unless wire.bytesize.between?(HEADER_SIZE, MAX_WIRE_SIZE)
|
|
59
|
+
raise error, "invalid arena envelope size"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
mode, reserved, size, count = wire.byteslice(4, 12).unpack("Ca3VV")
|
|
63
|
+
unless mode <= 1 && reserved == "\0\0\0" &&
|
|
64
|
+
size.between?(1, MAX_BYTES) && count.between?(1, MAX_MESSAGES)
|
|
65
|
+
raise error, "invalid arena header"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
data = wire.byteslice(HEADER_SIZE..)
|
|
69
|
+
data = yield(data, size) if mode == 1
|
|
70
|
+
raise error, "arena decoded size mismatch" unless data.bytesize == size
|
|
71
|
+
|
|
72
|
+
decode_messages(data, count, max_message_size, error)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def decode_messages(data, count, max_message_size, error)
|
|
77
|
+
messages = []
|
|
78
|
+
parts = []
|
|
79
|
+
offset = 0
|
|
80
|
+
part_count = 0
|
|
81
|
+
spent = 0
|
|
82
|
+
|
|
83
|
+
while offset < data.bytesize
|
|
84
|
+
flags = data.getbyte(offset)
|
|
85
|
+
raise error, "invalid arena frame flags" unless (flags & ~3).zero?
|
|
86
|
+
|
|
87
|
+
header = (flags & 2).zero? ? 2 : 9
|
|
88
|
+
raise error, "truncated arena frame header" if offset + header > data.bytesize
|
|
89
|
+
|
|
90
|
+
size = header == 2 ? data.getbyte(offset + 1) : data.byteslice(offset + 1, 8).unpack1("Q>")
|
|
91
|
+
offset += header
|
|
92
|
+
raise error, "truncated arena frame body" if size > data.bytesize - offset
|
|
93
|
+
|
|
94
|
+
part_count += 1
|
|
95
|
+
raise error, "too many arena parts" if part_count > MAX_PARTS
|
|
96
|
+
|
|
97
|
+
spent += size + PART_CHARGE
|
|
98
|
+
if max_message_size && spent > max_message_size
|
|
99
|
+
raise error, "decompressed message size exceeds maximum"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
parts << data.byteslice(offset, size)
|
|
103
|
+
offset += size
|
|
104
|
+
next unless (flags & 1).zero?
|
|
105
|
+
|
|
106
|
+
messages << parts
|
|
107
|
+
raise error, "too many arena messages" if messages.size > count
|
|
108
|
+
|
|
109
|
+
parts = []
|
|
110
|
+
spent = 0
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
unless parts.empty? && messages.size == count
|
|
114
|
+
raise error, "incomplete arena messages"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
messages
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -62,6 +62,7 @@ module OMQ
|
|
|
62
62
|
@peer_identity = peer_identity || "".b
|
|
63
63
|
@peer_socket_type = peer_type
|
|
64
64
|
@closed = false
|
|
65
|
+
@closed_signal = Async::Promise.new
|
|
65
66
|
@peer = nil
|
|
66
67
|
@direct_recv_queue = nil
|
|
67
68
|
@direct_recv_transform = nil
|
|
@@ -88,7 +89,7 @@ module OMQ
|
|
|
88
89
|
|
|
89
90
|
return unless @pending_direct
|
|
90
91
|
|
|
91
|
-
@pending_direct.each { |msg|
|
|
92
|
+
@pending_direct.each { |msg| deliver_direct(msg) }
|
|
92
93
|
@pending_direct = nil
|
|
93
94
|
end
|
|
94
95
|
|
|
@@ -115,11 +116,11 @@ module OMQ
|
|
|
115
116
|
end
|
|
116
117
|
|
|
117
118
|
if @direct_recv_queue
|
|
118
|
-
|
|
119
|
+
deliver_direct(parts)
|
|
119
120
|
elsif @send_queue
|
|
120
121
|
@send_queue.enqueue(parts)
|
|
121
122
|
else
|
|
122
|
-
(@pending_direct ||= []) <<
|
|
123
|
+
(@pending_direct ||= []) << parts
|
|
123
124
|
end
|
|
124
125
|
end
|
|
125
126
|
|
|
@@ -205,20 +206,33 @@ module OMQ
|
|
|
205
206
|
end
|
|
206
207
|
|
|
207
208
|
|
|
208
|
-
# Closes this pipe end and
|
|
209
|
+
# Closes this pipe end and wakes the peer's lifecycle and command reader.
|
|
209
210
|
#
|
|
210
211
|
# @return [void]
|
|
211
212
|
#
|
|
212
213
|
def close
|
|
213
214
|
return if @closed
|
|
214
215
|
@closed = true
|
|
216
|
+
@closed_signal.resolve(true)
|
|
215
217
|
@send_queue&.enqueue(nil) # close sentinel
|
|
216
218
|
end
|
|
217
219
|
|
|
218
220
|
|
|
221
|
+
# Lets the peer remove this link even when messages bypass recv pumps.
|
|
222
|
+
def wait_closed
|
|
223
|
+
@closed_signal.wait
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
|
|
219
227
|
private
|
|
220
228
|
|
|
221
229
|
|
|
230
|
+
def deliver_direct(parts)
|
|
231
|
+
transformed = apply_transform(parts)
|
|
232
|
+
@direct_recv_queue.enqueue(transformed) if transformed
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
|
|
222
236
|
def apply_transform(parts)
|
|
223
237
|
if @direct_recv_transform
|
|
224
238
|
@direct_recv_transform.call(parts)
|
data/lib/omq/transport/inproc.rb
CHANGED
|
@@ -130,7 +130,7 @@ module OMQ
|
|
|
130
130
|
"incompatible socket types: #{client_type} cannot connect to #{server_type}"
|
|
131
131
|
end
|
|
132
132
|
|
|
133
|
-
needs_cmds = needs_commands?(
|
|
133
|
+
needs_cmds = needs_commands?(client_type, server_type)
|
|
134
134
|
client_pipe, server_pipe = make_pipe_pair client_engine, server_engine,
|
|
135
135
|
client_type, server_type, needs_cmds
|
|
136
136
|
|
|
@@ -142,27 +142,12 @@ module OMQ
|
|
|
142
142
|
# Decides whether a Pipe pair needs command queues.
|
|
143
143
|
# Pipe's fast path skips queues entirely; command queues
|
|
144
144
|
# are only needed for socket types that exchange ZMTP commands
|
|
145
|
-
# (e.g. ROUTER/DEALER identity, PUB/SUB subscriptions)
|
|
146
|
-
# either side enables QoS ≥ 1.
|
|
145
|
+
# (e.g. ROUTER/DEALER identity, PUB/SUB subscriptions).
|
|
147
146
|
#
|
|
148
147
|
# @return [Boolean]
|
|
149
148
|
#
|
|
150
|
-
def needs_commands?(
|
|
151
|
-
|
|
152
|
-
return true if qos_enabled?(ce.options) || qos_enabled?(se.options)
|
|
153
|
-
false
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
# QoS integration: core +Options#qos+ defaults to Integer +0+.
|
|
158
|
-
# When the omq-qos extension is loaded, +#qos+ holds either
|
|
159
|
-
# +nil+ (QoS 0) or an +OMQ::QoS+ instance (levels 1–3). Treat
|
|
160
|
-
# both Integer 0 and nil as disabled.
|
|
161
|
-
def qos_enabled?(options)
|
|
162
|
-
q = options.qos
|
|
163
|
-
return false if q.nil?
|
|
164
|
-
return q != 0 if q.is_a?(Integer)
|
|
165
|
-
true
|
|
149
|
+
def needs_commands?(client_type, server_type)
|
|
150
|
+
COMMAND_TYPES.include?(client_type) || COMMAND_TYPES.include?(server_type)
|
|
166
151
|
end
|
|
167
152
|
|
|
168
153
|
|
data/lib/omq/version.rb
CHANGED
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.
|
|
4
|
+
version: 0.29.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrik Wenger
|
|
@@ -16,6 +16,9 @@ dependencies:
|
|
|
16
16
|
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
18
|
version: '0.10'
|
|
19
|
+
- - ">="
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: 0.10.5
|
|
19
22
|
type: :runtime
|
|
20
23
|
prerelease: false
|
|
21
24
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -23,6 +26,9 @@ dependencies:
|
|
|
23
26
|
- - "~>"
|
|
24
27
|
- !ruby/object:Gem::Version
|
|
25
28
|
version: '0.10'
|
|
29
|
+
- - ">="
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: 0.10.5
|
|
26
32
|
- !ruby/object:Gem::Dependency
|
|
27
33
|
name: async
|
|
28
34
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -114,6 +120,8 @@ files:
|
|
|
114
120
|
- lib/omq/scatter_gather.rb
|
|
115
121
|
- lib/omq/single_frame.rb
|
|
116
122
|
- lib/omq/socket.rb
|
|
123
|
+
- lib/omq/transport/compressed_connection.rb
|
|
124
|
+
- lib/omq/transport/compression_arena.rb
|
|
117
125
|
- lib/omq/transport/inproc.rb
|
|
118
126
|
- lib/omq/transport/inproc/pipe.rb
|
|
119
127
|
- lib/omq/transport/ipc.rb
|
|
@@ -139,7 +147,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
139
147
|
- !ruby/object:Gem::Version
|
|
140
148
|
version: '0'
|
|
141
149
|
requirements: []
|
|
142
|
-
rubygems_version: 4.0.
|
|
150
|
+
rubygems_version: 4.0.20
|
|
143
151
|
specification_version: 4
|
|
144
152
|
summary: Pure Ruby ZMQ library
|
|
145
153
|
test_files: []
|