omq 0.23.0 → 0.26.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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +144 -3
  3. data/README.md +23 -7
  4. data/lib/omq/client_server.rb +1 -1
  5. data/lib/omq/engine/connection_lifecycle.rb +12 -6
  6. data/lib/omq/engine/heartbeat.rb +1 -1
  7. data/lib/omq/engine/recv_pump.rb +29 -16
  8. data/lib/omq/engine.rb +6 -5
  9. data/lib/omq/ffi/engine.rb +646 -0
  10. data/lib/omq/ffi/libzmq.rb +134 -0
  11. data/lib/omq/ffi.rb +12 -0
  12. data/lib/omq/peer.rb +1 -1
  13. data/lib/omq/radio_dish.rb +1 -1
  14. data/lib/omq/readable.rb +5 -1
  15. data/lib/omq/routing/channel.rb +4 -4
  16. data/lib/omq/routing/client.rb +2 -2
  17. data/lib/omq/routing/conn_send_pump.rb +1 -1
  18. data/lib/omq/routing/dealer.rb +2 -2
  19. data/lib/omq/routing/dish.rb +2 -2
  20. data/lib/omq/routing/fan_out.rb +7 -7
  21. data/lib/omq/routing/gather.rb +2 -2
  22. data/lib/omq/routing/pair.rb +4 -4
  23. data/lib/omq/routing/peer.rb +2 -2
  24. data/lib/omq/routing/pub.rb +2 -2
  25. data/lib/omq/routing/pull.rb +2 -2
  26. data/lib/omq/routing/push.rb +3 -3
  27. data/lib/omq/routing/radio.rb +2 -2
  28. data/lib/omq/routing/rep.rb +2 -2
  29. data/lib/omq/routing/req.rb +2 -2
  30. data/lib/omq/routing/round_robin.rb +4 -4
  31. data/lib/omq/routing/router.rb +2 -2
  32. data/lib/omq/routing/scatter.rb +4 -5
  33. data/lib/omq/routing/server.rb +2 -2
  34. data/lib/omq/routing/sub.rb +2 -2
  35. data/lib/omq/routing/xpub.rb +2 -2
  36. data/lib/omq/routing/xsub.rb +2 -2
  37. data/lib/omq/socket.rb +2 -1
  38. data/lib/omq/transport/inproc/{direct_pipe.rb → pipe.rb} +22 -9
  39. data/lib/omq/transport/inproc.rb +26 -14
  40. data/lib/omq/transport/udp.rb +1 -1
  41. data/lib/omq/version.rb +1 -1
  42. data/lib/omq/writable.rb +32 -43
  43. metadata +5 -2
@@ -43,7 +43,7 @@ module OMQ
43
43
  end
44
44
 
45
45
 
46
- # @param connection [Connection]
46
+ # @param connection [Protocol::ZMTP::Connection]
47
47
  #
48
48
  def connection_added(connection)
49
49
  @connections << connection
@@ -56,7 +56,7 @@ module OMQ
56
56
  end
57
57
 
58
58
 
59
- # @param connection [Connection]
59
+ # @param connection [Protocol::ZMTP::Connection]
60
60
  #
61
61
  def connection_removed(connection)
62
62
  @connections.delete(connection)
data/lib/omq/socket.rb CHANGED
@@ -36,7 +36,7 @@ module OMQ
36
36
 
37
37
 
38
38
  # @return [Engine] the socket's engine. Exposed for peer tooling
39
- # (omq-cli, omq-ffi, omq-ractor) that needs to reach into the
39
+ # (omq-cli, omq-ractor) that needs to reach into the
40
40
  # socket's internals — not part of the stable user API.
41
41
  #
42
42
  attr_reader :engine
@@ -318,6 +318,7 @@ module OMQ
318
318
  when nil, :ruby
319
319
  Engine.new(socket_type, @options)
320
320
  when :ffi
321
+ require "omq/ffi" unless defined?(FFI::Engine)
321
322
  FFI::Engine.new(socket_type, @options)
322
323
  else
323
324
  raise ArgumentError, "unknown backend: #{backend}"
@@ -14,7 +14,7 @@ module OMQ
14
14
  # This reduces inproc from 3 queue hops to 2 (send_queue →
15
15
  # recv_queue), eliminating the internal pipe queue in between.
16
16
  #
17
- class DirectPipe
17
+ class Pipe
18
18
  # @return [String] peer's socket type
19
19
  #
20
20
  attr_reader :peer_socket_type
@@ -39,7 +39,7 @@ module OMQ
39
39
  attr_reader :peer_identity
40
40
 
41
41
 
42
- # @return [DirectPipe, nil] the other end of this pipe pair
42
+ # @return [Pipe, nil] the other end of this pipe pair
43
43
  #
44
44
  attr_accessor :peer
45
45
 
@@ -85,6 +85,7 @@ module OMQ
85
85
  def wire_direct_recv(queue, transform)
86
86
  @direct_recv_transform = transform
87
87
  @direct_recv_queue = queue
88
+
88
89
  return unless @pending_direct
89
90
 
90
91
  @pending_direct.each { |msg| queue.enqueue(msg) }
@@ -99,6 +100,20 @@ module OMQ
99
100
  #
100
101
  def send_message(parts)
101
102
  raise IOError, "closed" if @closed
103
+
104
+ # Writable#send guarantees frozen parts, but a frozen non-BINARY
105
+ # part (e.g. a `# frozen_string_literal: true` literal) can't be
106
+ # re-tagged in place. Inproc receivers see the parts directly, so
107
+ # upgrade that one case to fresh BINARY copies to keep the
108
+ # receive contract uniform with TCP/IPC.
109
+ #
110
+ # Non-String parts pass through untouched — plugins like
111
+ # omq-ractor's ShareableConnection carry arbitrary Ruby objects
112
+ # over inproc.
113
+ if parts.any? { |p| p.is_a?(String) && p.encoding != Encoding::BINARY }
114
+ parts = parts.map { |p| !p.is_a?(String) || p.encoding == Encoding::BINARY ? p : p.b.freeze }.freeze
115
+ end
116
+
102
117
  if @direct_recv_queue
103
118
  @direct_recv_queue.enqueue(apply_transform(parts))
104
119
  elsif @send_queue
@@ -114,7 +129,7 @@ module OMQ
114
129
 
115
130
  # Batched form, for parity with Protocol::ZMTP::Connection. The
116
131
  # work-stealing pumps call this when they dequeue more than one
117
- # message at once; DirectPipe just loops — no mutex to amortize.
132
+ # message at once; Pipe just loops — no mutex to amortize.
118
133
  #
119
134
  # @param messages [Array<Array<String>>]
120
135
  # @return [void]
@@ -147,14 +162,13 @@ module OMQ
147
162
  #
148
163
  def receive_message
149
164
  loop do
150
- item = @receive_queue.dequeue
151
-
152
- raise EOFError, "connection closed" if item.nil?
165
+ item = @receive_queue.dequeue or raise EOFError, "connection closed"
153
166
 
154
167
  if item.is_a?(Array) && item.first == :command
155
168
  if block_given?
156
169
  yield Protocol::ZMTP::Codec::Frame.new(item[1].to_body, command: true)
157
170
  end
171
+
158
172
  next
159
173
  end
160
174
 
@@ -181,8 +195,7 @@ module OMQ
181
195
  # @return [Protocol::ZMTP::Codec::Frame]
182
196
  #
183
197
  def read_frame
184
- item = @receive_queue.dequeue
185
- raise EOFError, "connection closed" if item.nil?
198
+ item = @receive_queue.dequeue or raise EOFError, "connection closed"
186
199
 
187
200
  if item.is_a?(Array) && item.first == :command
188
201
  Protocol::ZMTP::Codec::Frame.new(item[1].to_body, command: true)
@@ -208,7 +221,7 @@ module OMQ
208
221
 
209
222
  def apply_transform(parts)
210
223
  if @direct_recv_transform
211
- @direct_recv_transform.call(parts).freeze
224
+ @direct_recv_transform.call(parts)
212
225
  else
213
226
  parts
214
227
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "async"
4
4
  require "async/queue"
5
- require_relative "inproc/direct_pipe"
5
+ require_relative "inproc/pipe"
6
6
 
7
7
  module OMQ
8
8
  module Transport
@@ -11,8 +11,8 @@ module OMQ
11
11
  # Both peers are Ruby backend sockets in the same process (native
12
12
  # ZMQ's inproc registry is separate and unreachable). Messages are
13
13
  # transferred as Ruby arrays — no ZMTP framing, no byte
14
- # serialization. String parts are frozen by Writable#send to
15
- # prevent shared mutable state without copying.
14
+ # serialization. Parts are already frozen by Writable#send, so the
15
+ # receiver sees the same immutable contract as ZMTP transports.
16
16
  #
17
17
  module Inproc
18
18
  Engine.transports["inproc"] = self
@@ -122,8 +122,8 @@ module OMQ
122
122
  end
123
123
 
124
124
 
125
- # Decides whether a DirectPipe pair needs command queues.
126
- # DirectPipe's fast path skips queues entirely; command queues
125
+ # Decides whether a Pipe pair needs command queues.
126
+ # Pipe's fast path skips queues entirely; command queues
127
127
  # are only needed for socket types that exchange ZMTP commands
128
128
  # (e.g. ROUTER/DEALER identity, PUB/SUB subscriptions) or when
129
129
  # either side enables QoS ≥ 1.
@@ -132,16 +132,28 @@ module OMQ
132
132
  #
133
133
  def needs_commands?(ce, se, ct, st)
134
134
  return true if COMMAND_TYPES.include?(ct) || COMMAND_TYPES.include?(st)
135
- return true if ce.options.qos >= 1 || se.options.qos >= 1
135
+ return true if qos_enabled?(ce.options) || qos_enabled?(se.options)
136
136
  false
137
137
  end
138
138
 
139
139
 
140
- # Builds a bidirectional {DirectPipe} pair for client + server.
140
+ # QoS integration: core +Options#qos+ defaults to Integer +0+.
141
+ # When the omq-qos extension is loaded, +#qos+ holds either
142
+ # +nil+ (QoS 0) or an +OMQ::QoS+ instance (levels 1–3). Treat
143
+ # both Integer 0 and nil as disabled.
144
+ def qos_enabled?(options)
145
+ q = options.qos
146
+ return false if q.nil?
147
+ return q != 0 if q.is_a?(Integer)
148
+ true
149
+ end
150
+
151
+
152
+ # Builds a bidirectional {Pipe} pair for client + server.
141
153
  # When +needs_cmds+ is false the pipes have no command queues
142
154
  # (fast path — all traffic bypasses Async::Queue entirely).
143
155
  #
144
- # @return [Array(DirectPipe, DirectPipe)] client, server
156
+ # @return [Array(Pipe, Pipe)] client, server
145
157
  #
146
158
  def make_pipe_pair(ce, se, ct, st, needs_cmds)
147
159
  if needs_cmds
@@ -149,12 +161,12 @@ module OMQ
149
161
  b_to_a = Async::Queue.new
150
162
  end
151
163
 
152
- client = DirectPipe.new(send_queue: needs_cmds ? a_to_b : nil,
153
- receive_queue: needs_cmds ? b_to_a : nil,
154
- peer_identity: se.options.identity, peer_type: st.to_s)
155
- server = DirectPipe.new(send_queue: needs_cmds ? b_to_a : nil,
156
- receive_queue: needs_cmds ? a_to_b : nil,
157
- peer_identity: ce.options.identity, peer_type: ct.to_s)
164
+ client = Pipe.new(send_queue: needs_cmds ? a_to_b : nil,
165
+ receive_queue: needs_cmds ? b_to_a : nil,
166
+ peer_identity: se.options.identity, peer_type: st.to_s)
167
+ server = Pipe.new(send_queue: needs_cmds ? b_to_a : nil,
168
+ receive_queue: needs_cmds ? a_to_b : nil,
169
+ peer_identity: ce.options.identity, peer_type: ct.to_s)
158
170
 
159
171
  client.peer = server
160
172
  server.peer = client
@@ -200,7 +200,7 @@ module OMQ
200
200
  next unless parts
201
201
  group, body = parts
202
202
  next unless @groups.include?(group.b)
203
- return [group.b.freeze, body.b.freeze]
203
+ return [group, body]
204
204
  rescue IO::WaitReadable
205
205
  @socket.wait_readable
206
206
  retry
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.23.0"
4
+ VERSION = "0.26.2"
5
5
  end
data/lib/omq/writable.rb CHANGED
@@ -9,19 +9,45 @@ module OMQ
9
9
  include QueueWritable
10
10
 
11
11
 
12
- EMPTY_PART = "".b.freeze
13
-
14
-
15
12
  # Sends a message.
16
13
  #
17
- # @param message [String, Array<String>] message parts
14
+ # Parts must be String-like (respond to `#to_str`). Use an empty
15
+ # string to send an empty frame — `nil` raises `NoMethodError` so
16
+ # accidental nils surface instead of silently producing a zero-byte
17
+ # frame. Invariants after `#send` returns:
18
+ #
19
+ # * every part is a frozen String
20
+ # * unfrozen String parts are re-tagged to `Encoding::BINARY` in
21
+ # place (a flag flip, no copy)
22
+ # * the parts array (if the caller passed one) is frozen
23
+ #
24
+ # The receiver always gets frozen `BINARY`-tagged parts — on TCP/IPC
25
+ # via byteslice on the wire, on inproc via {Pipe#send_message} which
26
+ # duplicates the one pathological case (frozen non-BINARY parts) so
27
+ # the receiver sees BINARY like every other transport.
28
+ #
29
+ # @param message [String, #to_str, Array<String, #to_str>]
18
30
  # @return [self]
19
31
  # @raise [IO::TimeoutError] if write_timeout exceeded
32
+ # @raise [NoMethodError] if a part is not String-like
20
33
  #
21
34
  def send(message)
22
- parts = freeze_message(message)
35
+ parts = message.is_a?(Array) ? message : [message]
36
+ raise ArgumentError, "message has no parts" if parts.empty?
37
+
38
+ parts = parts.map { |p| p.to_str } if parts.any? { |p| !p.is_a?(String) }
23
39
 
24
- Reactor.run timeout: @options.write_timeout do |task|
40
+ parts.each do |part|
41
+ part.force_encoding(Encoding::BINARY) unless part.frozen? || part.encoding == Encoding::BINARY
42
+ part.freeze
43
+ end
44
+ parts.freeze
45
+
46
+ if @engine.on_io_thread?
47
+ Reactor.run(timeout: @options.write_timeout) { @engine.enqueue_send(parts) }
48
+ elsif (timeout = @options.write_timeout)
49
+ Async::Task.current.with_timeout(timeout, IO::TimeoutError) { @engine.enqueue_send(parts) }
50
+ else
25
51
  @engine.enqueue_send(parts)
26
52
  end
27
53
 
@@ -48,42 +74,5 @@ module OMQ
48
74
  true
49
75
  end
50
76
 
51
-
52
- private
53
-
54
-
55
- # Converts a message into a frozen array of frozen binary strings.
56
- #
57
- # @param message [String, Array<String>]
58
- # @return [Array<String>] frozen array of frozen binary strings
59
- #
60
- def freeze_message(message)
61
- parts = message.is_a?(Array) ? message : [message]
62
- raise ArgumentError, "message has no parts" if parts.empty?
63
-
64
- all_ready = parts.all? { |p| p.is_a?(String) && p.frozen? && p.encoding == Encoding::BINARY }
65
-
66
- # Already a frozen array of frozen binary strings → return as-is.
67
- return parts if all_ready && parts.frozen?
68
-
69
- # Items are ready; just freeze the outer array.
70
- return parts.freeze if all_ready
71
-
72
- # Items need conversion. Mutate in place when we can.
73
- if parts.frozen?
74
- parts.map { |p| frozen_binary(p) }.freeze
75
- else
76
- parts.map! { |p| frozen_binary(p) }.freeze
77
- end
78
- end
79
-
80
-
81
- def frozen_binary(obj)
82
- return EMPTY_PART if obj.nil?
83
- s = obj.to_s
84
- return s if s.frozen? && s.encoding == Encoding::BINARY
85
- s.b.freeze
86
- end
87
-
88
77
  end
89
78
  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.23.0
4
+ version: 0.26.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrik Wenger
@@ -76,6 +76,9 @@ files:
76
76
  - lib/omq/engine/reconnect.rb
77
77
  - lib/omq/engine/recv_pump.rb
78
78
  - lib/omq/engine/socket_lifecycle.rb
79
+ - lib/omq/ffi.rb
80
+ - lib/omq/ffi/engine.rb
81
+ - lib/omq/ffi/libzmq.rb
79
82
  - lib/omq/options.rb
80
83
  - lib/omq/pair.rb
81
84
  - lib/omq/peer.rb
@@ -114,7 +117,7 @@ files:
114
117
  - lib/omq/single_frame.rb
115
118
  - lib/omq/socket.rb
116
119
  - lib/omq/transport/inproc.rb
117
- - lib/omq/transport/inproc/direct_pipe.rb
120
+ - lib/omq/transport/inproc/pipe.rb
118
121
  - lib/omq/transport/ipc.rb
119
122
  - lib/omq/transport/tcp.rb
120
123
  - lib/omq/transport/udp.rb