omq 0.19.0 → 0.19.3

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.
@@ -6,6 +6,8 @@ module OMQ
6
6
  #
7
7
  class Pull
8
8
  include FairRecv
9
+
10
+
9
11
  # @param engine [Engine]
10
12
  #
11
13
  def initialize(engine)
@@ -19,6 +21,7 @@ module OMQ
19
21
  #
20
22
  attr_reader :recv_queue
21
23
 
24
+
22
25
  # @param connection [Connection]
23
26
  #
24
27
  def connection_added(connection)
@@ -49,6 +52,7 @@ module OMQ
49
52
  @tasks.each(&:stop)
50
53
  @tasks.clear
51
54
  end
55
+
52
56
  end
53
57
  end
54
58
  end
@@ -7,6 +7,7 @@ module OMQ
7
7
  class Push
8
8
  include RoundRobin
9
9
 
10
+
10
11
  # @param engine [Engine]
11
12
  #
12
13
  def initialize(engine)
@@ -16,13 +17,23 @@ module OMQ
16
17
  end
17
18
 
18
19
 
19
- # PUSH is write-only.
20
+ # PUSH is write-only. Engine-facing recv contract: dequeue raises,
21
+ # unblock is a no-op (fatal-error propagation still calls it).
20
22
  #
21
23
  def recv_queue
22
24
  raise "PUSH sockets cannot receive"
23
25
  end
24
26
 
25
27
 
28
+ def dequeue_recv
29
+ raise "PUSH sockets cannot receive"
30
+ end
31
+
32
+
33
+ def unblock_recv
34
+ end
35
+
36
+
26
37
  # @param connection [Connection]
27
38
  #
28
39
  def connection_added(connection)
@@ -53,6 +64,7 @@ module OMQ
53
64
  @tasks.clear
54
65
  end
55
66
 
67
+
56
68
  private
57
69
 
58
70
 
@@ -66,6 +78,7 @@ module OMQ
66
78
  conn.receive_message # blocks until peer disconnects; then exits
67
79
  end
68
80
  end
81
+
69
82
  end
70
83
  end
71
84
  end
@@ -14,6 +14,11 @@ module OMQ
14
14
  EMPTY_FRAME = "".b.freeze
15
15
 
16
16
 
17
+ # @return [FairQueue]
18
+ #
19
+ attr_reader :recv_queue
20
+
21
+
17
22
  # @param engine [Engine]
18
23
  #
19
24
  def initialize(engine)
@@ -26,17 +31,14 @@ module OMQ
26
31
  end
27
32
 
28
33
 
29
- # @return [FairQueue]
30
- #
31
- attr_reader :recv_queue
32
-
33
34
  # @param connection [Connection]
34
35
  #
35
36
  def connection_added(connection)
36
37
  add_fair_recv_connection(connection) do |msg|
37
- delimiter = msg.index(&:empty?) || msg.size
38
+ delimiter = msg.index { |p| p.empty? } || msg.size
38
39
  envelope = msg[0, delimiter]
39
40
  body = msg[(delimiter + 1)..] || []
41
+
40
42
  @pending_replies << { conn: connection, envelope: envelope }
41
43
  body
42
44
  end
@@ -14,6 +14,11 @@ module OMQ
14
14
  EMPTY_BINARY = ::Protocol::ZMTP::Codec::EMPTY_BINARY
15
15
 
16
16
 
17
+ # @return [FairQueue]
18
+ #
19
+ attr_reader :recv_queue
20
+
21
+
17
22
  # @param engine [Engine]
18
23
  #
19
24
  def initialize(engine)
@@ -25,11 +30,6 @@ module OMQ
25
30
  end
26
31
 
27
32
 
28
- # @return [FairQueue]
29
- #
30
- attr_reader :recv_queue
31
-
32
-
33
33
  # @param connection [Connection]
34
34
  #
35
35
  def connection_added(connection)
@@ -19,6 +19,9 @@ module OMQ
19
19
  # their #initialize.
20
20
  #
21
21
  module RoundRobin
22
+ BATCH_MSG_CAP = 256
23
+ BATCH_BYTE_CAP = 512 * 1024
24
+
22
25
  # @return [Boolean] true when the shared send queue is empty
23
26
  # and no pump fiber is mid-write with a dequeued batch.
24
27
  #
@@ -26,8 +29,10 @@ module OMQ
26
29
  @send_queue.empty? && @in_flight == 0
27
30
  end
28
31
 
32
+
29
33
  private
30
34
 
35
+
31
36
  # Initializes the shared send queue for the including class.
32
37
  #
33
38
  # @param engine [Engine]
@@ -83,6 +88,7 @@ module OMQ
83
88
  #
84
89
  def enqueue_round_robin(parts)
85
90
  pipe = @direct_pipe
91
+
86
92
  if pipe&.direct_recv_queue
87
93
  pipe.send_message(transform_send(parts))
88
94
  else
@@ -97,7 +103,9 @@ module OMQ
97
103
  # @param parts [Array<String>]
98
104
  # @return [Array<String>]
99
105
  #
100
- def transform_send(parts) = parts
106
+ def transform_send(parts)
107
+ parts
108
+ end
101
109
 
102
110
 
103
111
  # Spawns a send pump for one connection. Drains the shared send
@@ -129,34 +137,47 @@ module OMQ
129
137
  batch = [@send_queue.dequeue]
130
138
  drain_send_queue_capped(batch)
131
139
  @in_flight += batch.size
140
+
132
141
  begin
133
142
  write_batch(conn, batch)
134
143
  ensure
135
144
  @in_flight -= batch.size
136
145
  end
137
- batch.each { |parts| @engine.emit_verbose_msg_sent(conn, parts) }
146
+
147
+ batch.each do |parts|
148
+ @engine.emit_verbose_msg_sent(conn, parts)
149
+ end
150
+
138
151
  Async::Task.current.yield
139
152
  end
140
153
  end
154
+
141
155
  @conn_send_tasks[conn] = task
142
156
  @tasks << task
143
157
  end
144
158
 
145
159
 
146
- BATCH_MSG_CAP = 256
147
- BATCH_BYTE_CAP = 512 * 1024
148
-
149
160
  def drain_send_queue_capped(batch)
150
- bytes = batch[0].sum(&:bytesize)
161
+ bytes = batch_bytes(batch[0])
151
162
  while batch.size < BATCH_MSG_CAP && bytes < BATCH_BYTE_CAP
152
163
  msg = @send_queue.dequeue(timeout: 0)
153
164
  break unless msg
154
165
  batch << msg
155
- bytes += msg.sum(&:bytesize)
166
+ bytes += batch_bytes(msg)
156
167
  end
157
168
  end
158
169
 
159
170
 
171
+ # Byte accounting for send-queue batching. Connection wrappers
172
+ # (e.g. OMQ::Ractor's MarshalConnection) may enqueue non-string
173
+ # parts that get transformed at write time — skip those for the
174
+ # fairness cap rather than crashing on #bytesize.
175
+ #
176
+ def batch_bytes(parts)
177
+ parts.sum { |p| p.respond_to?(:bytesize) ? p.bytesize : 0 }
178
+ end
179
+
180
+
160
181
  def write_batch(conn, batch)
161
182
  if batch.size == 1
162
183
  conn.send_message(transform_send(batch[0]))
@@ -165,6 +186,7 @@ module OMQ
165
186
  conn.flush
166
187
  end
167
188
  end
189
+
168
190
  end
169
191
  end
170
192
  end
@@ -12,6 +12,13 @@ module OMQ
12
12
  #
13
13
  class Router
14
14
  include FairRecv
15
+
16
+
17
+ # @return [FairQueue]
18
+ #
19
+ attr_reader :recv_queue
20
+
21
+
15
22
  # @param engine [Engine]
16
23
  #
17
24
  def initialize(engine)
@@ -25,10 +32,6 @@ module OMQ
25
32
  end
26
33
 
27
34
 
28
- # @return [FairQueue]
29
- #
30
- attr_reader :recv_queue
31
-
32
35
  # @param connection [Connection]
33
36
  #
34
37
  def connection_added(connection)
@@ -8,6 +8,11 @@ module OMQ
8
8
  #
9
9
  class Sub
10
10
 
11
+ # @return [FairQueue]
12
+ #
13
+ attr_reader :recv_queue
14
+
15
+
11
16
  # @param engine [Engine]
12
17
  #
13
18
  def initialize(engine)
@@ -19,9 +24,17 @@ module OMQ
19
24
  end
20
25
 
21
26
 
22
- # @return [FairQueue]
27
+ # Engine-facing recv contract. Delegates to the FairQueue.
23
28
  #
24
- attr_reader :recv_queue
29
+ def dequeue_recv
30
+ @recv_queue.dequeue
31
+ end
32
+
33
+
34
+ def unblock_recv
35
+ @recv_queue.push(nil)
36
+ end
37
+
25
38
 
26
39
  # @param connection [Connection]
27
40
  #
@@ -85,6 +98,7 @@ module OMQ
85
98
  @tasks.each(&:stop)
86
99
  @tasks.clear
87
100
  end
101
+
88
102
  end
89
103
  end
90
104
  end
@@ -14,6 +14,11 @@ module OMQ
14
14
  class XPub
15
15
  include FanOut
16
16
 
17
+ # @return [Async::LimitedQueue]
18
+ #
19
+ attr_reader :recv_queue
20
+
21
+
17
22
  # @param engine [Engine]
18
23
  #
19
24
  def initialize(engine)
@@ -24,9 +29,17 @@ module OMQ
24
29
  end
25
30
 
26
31
 
27
- # @return [Async::LimitedQueue]
32
+ # Engine-facing recv contract. Delegates to the bounded queue.
28
33
  #
29
- attr_reader :recv_queue
34
+ def dequeue_recv
35
+ @recv_queue.dequeue
36
+ end
37
+
38
+
39
+ def unblock_recv
40
+ @recv_queue.push(nil)
41
+ end
42
+
30
43
 
31
44
  # @param connection [Connection]
32
45
  #
@@ -63,8 +76,10 @@ module OMQ
63
76
  @tasks.clear
64
77
  end
65
78
 
79
+
66
80
  private
67
81
 
82
+
68
83
  # Expose subscription to application as data message.
69
84
  #
70
85
  def on_subscribe(conn, prefix)
@@ -79,6 +94,7 @@ module OMQ
79
94
  super
80
95
  @recv_queue.enqueue(["\x00#{prefix}".b])
81
96
  end
97
+
82
98
  end
83
99
  end
84
100
  end
@@ -10,6 +10,11 @@ module OMQ
10
10
  #
11
11
  class XSub
12
12
 
13
+ # @return [FairQueue]
14
+ #
15
+ attr_reader :recv_queue
16
+
17
+
13
18
  # @param engine [Engine]
14
19
  #
15
20
  def initialize(engine)
@@ -22,9 +27,17 @@ module OMQ
22
27
  end
23
28
 
24
29
 
25
- # @return [FairQueue]
30
+ # Engine-facing recv contract. Delegates to the FairQueue.
26
31
  #
27
- attr_reader :recv_queue
32
+ def dequeue_recv
33
+ @recv_queue.dequeue
34
+ end
35
+
36
+
37
+ def unblock_recv
38
+ @recv_queue.push(nil)
39
+ end
40
+
28
41
 
29
42
  # @param connection [Connection]
30
43
  #
@@ -58,7 +71,9 @@ module OMQ
58
71
  # @param parts [Array<String>]
59
72
  #
60
73
  def enqueue(parts)
61
- @connections.each { |conn| @conn_queues[conn]&.enqueue(parts) }
74
+ @connections.each do |conn|
75
+ @conn_queues[conn]&.enqueue(parts)
76
+ end
62
77
  end
63
78
 
64
79
 
@@ -78,16 +93,21 @@ module OMQ
78
93
  @conn_queues.values.all?(&:empty?)
79
94
  end
80
95
 
96
+
81
97
  private
82
98
 
99
+
83
100
  def start_conn_send_pump(conn, q)
84
101
  task = @engine.spawn_conn_pump_task(conn, annotation: "send pump") do
85
102
  loop do
86
103
  parts = q.dequeue
87
104
  frame = parts.first&.b
105
+
88
106
  next if frame.nil? || frame.empty?
107
+
89
108
  flag = frame.getbyte(0)
90
109
  prefix = frame.byteslice(1..) || "".b
110
+
91
111
  case flag
92
112
  when 0x01
93
113
  conn.send_command(Protocol::ZMTP::Codec::Command.subscribe(prefix))
@@ -96,9 +116,11 @@ module OMQ
96
116
  end
97
117
  end
98
118
  end
119
+
99
120
  @conn_send_tasks[conn] = task
100
121
  @tasks << task
101
122
  end
123
+
102
124
  end
103
125
  end
104
126
  end
data/lib/omq/routing.rb CHANGED
@@ -110,5 +110,6 @@ module OMQ
110
110
  @registry[socket_type] or raise ArgumentError, "unknown socket type: #{socket_type.inspect}"
111
111
  end
112
112
  end
113
+
113
114
  end
114
115
  end
@@ -19,5 +19,6 @@ module OMQ
19
19
  end
20
20
  super
21
21
  end
22
+
22
23
  end
23
24
  end
data/lib/omq/socket.rb CHANGED
@@ -6,6 +6,30 @@ module OMQ
6
6
  # Socket base class.
7
7
  #
8
8
  class Socket
9
+ extend Forwardable
10
+
11
+ # Creates a new socket and binds it to the given endpoint.
12
+ #
13
+ # @param endpoint [String]
14
+ # @param opts [Hash] keyword arguments forwarded to {#initialize}
15
+ # @return [Socket]
16
+ #
17
+ def self.bind(endpoint, **opts)
18
+ new("@#{endpoint}", **opts)
19
+ end
20
+
21
+
22
+ # Creates a new socket and connects it to the given endpoint.
23
+ #
24
+ # @param endpoint [String]
25
+ # @param opts [Hash] keyword arguments forwarded to {#initialize}
26
+ # @return [Socket]
27
+ #
28
+ def self.connect(endpoint, **opts)
29
+ new(">#{endpoint}", **opts)
30
+ end
31
+
32
+
9
33
  # @return [Options]
10
34
  #
11
35
  attr_reader :options
@@ -16,10 +40,15 @@ module OMQ
16
40
  attr_reader :last_tcp_port
17
41
 
18
42
 
19
- # Delegate socket option accessors to @options.
43
+ # @return [Engine] the socket's engine. Exposed for peer tooling
44
+ # (omq-cli, omq-ffi, omq-ractor) that needs to reach into the
45
+ # socket's internals — not part of the stable user API.
20
46
  #
21
- extend Forwardable
47
+ attr_reader :engine
22
48
 
49
+
50
+ # Delegate socket option accessors to @options.
51
+ #
23
52
  def_delegators :@options,
24
53
  :send_hwm, :send_hwm=,
25
54
  :recv_hwm, :recv_hwm=,
@@ -42,28 +71,6 @@ module OMQ
42
71
  :mechanism, :mechanism=
43
72
 
44
73
 
45
- # Creates a new socket and binds it to the given endpoint.
46
- #
47
- # @param endpoint [String]
48
- # @param opts [Hash] keyword arguments forwarded to {#initialize}
49
- # @return [Socket]
50
- #
51
- def self.bind(endpoint, **opts)
52
- new("@#{endpoint}", **opts)
53
- end
54
-
55
-
56
- # Creates a new socket and connects it to the given endpoint.
57
- #
58
- # @param endpoint [String]
59
- # @param opts [Hash] keyword arguments forwarded to {#initialize}
60
- # @return [Socket]
61
- #
62
- def self.connect(endpoint, **opts)
63
- new(">#{endpoint}", **opts)
64
- end
65
-
66
-
67
74
  # @param endpoints [String, nil] optional endpoint with prefix convention
68
75
  # (+@+ for bind, +>+ for connect, plain uses subclass default)
69
76
  # @param linger [Integer] linger period in seconds (default 0)
@@ -134,19 +141,27 @@ module OMQ
134
141
 
135
142
 
136
143
  # @return [Async::Promise] resolves when first peer completes handshake
137
- def peer_connected = @engine.peer_connected
144
+ def peer_connected
145
+ @engine.peer_connected
146
+ end
138
147
 
139
148
 
140
149
  # @return [Async::Promise] resolves when first subscriber joins (PUB/XPUB only)
141
- def subscriber_joined = @engine.routing.subscriber_joined
150
+ def subscriber_joined
151
+ @engine.routing.subscriber_joined
152
+ end
142
153
 
143
154
 
144
155
  # @return [Async::Promise] resolves when all peers disconnect (after having had peers)
145
- def all_peers_gone = @engine.all_peers_gone
156
+ def all_peers_gone
157
+ @engine.all_peers_gone
158
+ end
146
159
 
147
160
 
148
161
  # @return [Integer] current number of peer connections
149
- def connection_count = @engine.connections.size
162
+ def connection_count
163
+ @engine.connections.size
164
+ end
150
165
 
151
166
 
152
167
  # Signals end-of-stream on the receive side. A subsequent
@@ -263,6 +278,7 @@ module OMQ
263
278
  #
264
279
  def attach_endpoints(endpoints, default:)
265
280
  return unless endpoints
281
+
266
282
  case endpoints
267
283
  when /\A@(.+)\z/
268
284
  bind($1)
@@ -311,5 +327,6 @@ module OMQ
311
327
  def ensure_parent_task(parent: nil)
312
328
  @engine.capture_parent_task(parent: parent)
313
329
  end
330
+
314
331
  end
315
332
  end
@@ -110,14 +110,18 @@ module OMQ
110
110
 
111
111
  # @return [Boolean] always false; inproc pipes are never encrypted
112
112
  #
113
- def encrypted? = false
113
+ def encrypted?
114
+ false
115
+ end
114
116
 
115
117
 
116
118
  # No-op — inproc has no IO buffer to flush.
117
119
  #
118
120
  # @return [nil]
119
121
  #
120
- def flush = nil
122
+ def flush
123
+ nil
124
+ end
121
125
 
122
126
 
123
127
  # Receives a multi-frame message.
@@ -210,6 +210,7 @@ module OMQ
210
210
  @tasks.each(&:stop)
211
211
  @servers.each { |s| s.close rescue nil }
212
212
  end
213
+
213
214
  end
214
215
  end
215
216
  end
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.19.0"
4
+ VERSION = "0.19.3"
5
5
  end
data/lib/omq/writable.rb CHANGED
@@ -7,6 +7,11 @@ module OMQ
7
7
  #
8
8
  module Writable
9
9
  include QueueWritable
10
+
11
+
12
+ EMPTY_PART = "".b.freeze
13
+
14
+
10
15
  # Sends a message.
11
16
  #
12
17
  # @param message [String, Array<String>] message parts
@@ -33,8 +38,20 @@ module OMQ
33
38
  send(message)
34
39
  end
35
40
 
41
+
42
+ # Waits until the socket is writable.
43
+ #
44
+ # @param timeout [Numeric, nil] timeout in seconds
45
+ # @return [true]
46
+ #
47
+ def wait_writable(timeout = @options.write_timeout)
48
+ true
49
+ end
50
+
51
+
36
52
  private
37
53
 
54
+
38
55
  # Converts a message into a frozen array of frozen binary strings.
39
56
  #
40
57
  # @param message [String, Array<String>]
@@ -44,21 +61,23 @@ module OMQ
44
61
  parts = message.is_a?(Array) ? message : [message]
45
62
  raise ArgumentError, "message has no parts" if parts.empty?
46
63
 
47
- # Fast path: skip map when all parts are already frozen binary.
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.
48
73
  if parts.frozen?
49
- return parts if parts.all? { |p| p.is_a?(String) && p.frozen? && p.encoding == Encoding::BINARY }
50
- parts = parts.map { |p| frozen_binary(p) }
74
+ parts.map { |p| frozen_binary(p) }.freeze
51
75
  else
52
- unless parts.all? { |p| p.is_a?(String) && p.frozen? && p.encoding == Encoding::BINARY }
53
- parts.map! { |p| frozen_binary(p) }
54
- end
76
+ parts.map! { |p| frozen_binary(p) }.freeze
55
77
  end
56
- parts.freeze
57
78
  end
58
79
 
59
80
 
60
- EMPTY_PART = "".b.freeze
61
-
62
81
  def frozen_binary(obj)
63
82
  return EMPTY_PART if obj.nil?
64
83
  s = obj.to_s
@@ -66,15 +85,5 @@ module OMQ
66
85
  s.b.freeze
67
86
  end
68
87
 
69
- public
70
-
71
- # Waits until the socket is writable.
72
- #
73
- # @param timeout [Numeric, nil] timeout in seconds
74
- # @return [true]
75
- #
76
- def wait_writable(timeout = @options.write_timeout)
77
- true
78
- end
79
88
  end
80
89
  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.19.0
4
+ version: 0.19.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrik Wenger