omq 0.22.1 → 0.23.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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +115 -0
  3. data/README.md +17 -21
  4. data/lib/omq/channel.rb +35 -0
  5. data/lib/omq/client_server.rb +72 -0
  6. data/lib/omq/constants.rb +68 -0
  7. data/lib/omq/engine/connection_lifecycle.rb +11 -3
  8. data/lib/omq/engine/heartbeat.rb +2 -3
  9. data/lib/omq/engine/maintenance.rb +4 -5
  10. data/lib/omq/engine/reconnect.rb +12 -11
  11. data/lib/omq/engine/recv_pump.rb +3 -4
  12. data/lib/omq/engine/socket_lifecycle.rb +26 -9
  13. data/lib/omq/engine.rb +196 -85
  14. data/lib/omq/peer.rb +49 -0
  15. data/lib/omq/pub_sub.rb +2 -2
  16. data/lib/omq/radio_dish.rb +122 -0
  17. data/lib/omq/reactor.rb +14 -5
  18. data/lib/omq/routing/channel.rb +110 -0
  19. data/lib/omq/routing/client.rb +70 -0
  20. data/lib/omq/routing/conn_send_pump.rb +4 -7
  21. data/lib/omq/routing/dealer.rb +1 -13
  22. data/lib/omq/routing/dish.rb +94 -0
  23. data/lib/omq/routing/fan_out.rb +5 -9
  24. data/lib/omq/routing/gather.rb +60 -0
  25. data/lib/omq/routing/pair.rb +3 -22
  26. data/lib/omq/routing/peer.rb +95 -0
  27. data/lib/omq/routing/pub.rb +0 -11
  28. data/lib/omq/routing/pull.rb +1 -13
  29. data/lib/omq/routing/push.rb +1 -10
  30. data/lib/omq/routing/radio.rb +187 -0
  31. data/lib/omq/routing/rep.rb +3 -17
  32. data/lib/omq/routing/req.rb +4 -16
  33. data/lib/omq/routing/round_robin.rb +11 -15
  34. data/lib/omq/routing/router.rb +3 -17
  35. data/lib/omq/routing/scatter.rb +77 -0
  36. data/lib/omq/routing/server.rb +90 -0
  37. data/lib/omq/routing/sub.rb +1 -13
  38. data/lib/omq/routing/xpub.rb +0 -11
  39. data/lib/omq/routing/xsub.rb +6 -23
  40. data/lib/omq/scatter_gather.rb +56 -0
  41. data/lib/omq/socket.rb +8 -23
  42. data/lib/omq/transport/inproc/direct_pipe.rb +17 -15
  43. data/lib/omq/transport/inproc.rb +11 -3
  44. data/lib/omq/transport/ipc.rb +41 -13
  45. data/lib/omq/transport/tcp.rb +59 -23
  46. data/lib/omq/transport/udp.rb +281 -0
  47. data/lib/omq/version.rb +1 -1
  48. data/lib/omq.rb +9 -64
  49. metadata +16 -2
  50. data/lib/omq/monitor_event.rb +0 -16
@@ -0,0 +1,281 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+ require "uri"
5
+ require "set"
6
+
7
+ module OMQ
8
+ module Transport
9
+ # UDP transport for RADIO/DISH sockets.
10
+ #
11
+ # Connectionless, datagram-based. No ZMTP handshake.
12
+ # DISH binds and receives; RADIO connects and sends.
13
+ #
14
+ # Wire format per datagram:
15
+ # flags (1 byte = 0x01) | group_size (1 byte) | group (n bytes) | body
16
+ #
17
+ module UDP
18
+ Engine.transports["udp"] = self
19
+
20
+
21
+ MAX_DATAGRAM_SIZE = 65507
22
+
23
+ class << self
24
+ # Creates a bound UDP listener for a DISH socket.
25
+ #
26
+ # @param endpoint [String] e.g. "udp://*:5555" or "udp://127.0.0.1:5555"
27
+ # @param engine [Engine]
28
+ # @return [Listener]
29
+ #
30
+ def listener(endpoint, engine, **)
31
+ host, port = parse_endpoint(endpoint)
32
+ host = "0.0.0.0" if host == "*"
33
+ socket = UDPSocket.new
34
+ socket.bind(host, port)
35
+ Listener.new(endpoint, socket, engine)
36
+ end
37
+
38
+
39
+ # Creates a UDP dialer for a RADIO socket.
40
+ #
41
+ # @param endpoint [String] e.g. "udp://127.0.0.1:5555"
42
+ # @param engine [Engine]
43
+ # @return [Dialer]
44
+ #
45
+ def dialer(endpoint, engine, **)
46
+ Dialer.new(endpoint, engine)
47
+ end
48
+
49
+
50
+ # @param endpoint [String]
51
+ # @return [Array(String, Integer)]
52
+ #
53
+ def parse_endpoint(endpoint)
54
+ uri = URI.parse(endpoint)
55
+ [uri.hostname, uri.port]
56
+ end
57
+
58
+
59
+ # Encodes a group + body into a UDP datagram.
60
+ #
61
+ # @param group [String]
62
+ # @param body [String]
63
+ # @return [String] binary datagram
64
+ #
65
+ def encode_datagram(group, body)
66
+ g = group.b
67
+ b = body.b
68
+ [0x01, g.bytesize].pack("CC") + g + b
69
+ end
70
+
71
+
72
+ # Decodes a UDP datagram into [group, body].
73
+ #
74
+ # @param data [String] raw datagram bytes
75
+ # @return [Array(String, String), nil] nil if malformed
76
+ #
77
+ def decode_datagram(data)
78
+ return nil if data.bytesize < 2
79
+ return nil unless data.getbyte(0) & 0x01 == 0x01
80
+ gs = data.getbyte(1)
81
+ return nil if data.bytesize < 2 + gs
82
+ group = data.byteslice(2, gs)
83
+ body = data.byteslice(2 + gs, data.bytesize - 2 - gs)
84
+ [group, body]
85
+ end
86
+ end
87
+
88
+
89
+ # A UDP dialer — registers a single RadioConnection with the engine.
90
+ #
91
+ # UDP is connectionless; #connect is a one-shot registration.
92
+ # The reconnect loop will break out after the first successful call.
93
+ #
94
+ class Dialer
95
+ # @return [String] the endpoint this dialer sends to
96
+ #
97
+ attr_reader :endpoint
98
+
99
+
100
+ # @param endpoint [String]
101
+ # @param engine [Engine]
102
+ #
103
+ def initialize(endpoint, engine)
104
+ @endpoint = endpoint
105
+ @engine = engine
106
+ end
107
+
108
+
109
+ # Registers a RadioConnection with the engine.
110
+ #
111
+ # @return [void]
112
+ #
113
+ def connect
114
+ host, port = UDP.parse_endpoint(@endpoint)
115
+ socket = UDPSocket.new
116
+ conn = RadioConnection.new(socket, host, port)
117
+ @engine.connection_ready(conn, endpoint: @endpoint)
118
+ end
119
+
120
+ end
121
+
122
+
123
+ # Outgoing UDP connection for RADIO sockets.
124
+ #
125
+ # Intentionally does not implement #read_frame — this signals
126
+ # Routing::Radio to skip the group listener and use ANY_GROUPS.
127
+ #
128
+ class RadioConnection
129
+ # @param socket [UDPSocket]
130
+ # @param host [String]
131
+ # @param port [Integer]
132
+ #
133
+ def initialize(socket, host, port)
134
+ @socket = socket
135
+ @host = host
136
+ @port = port
137
+ end
138
+
139
+
140
+ # Encodes and sends a datagram.
141
+ #
142
+ # @param parts [Array<String>] [group, body]
143
+ #
144
+ def write_message(parts)
145
+ group, body = parts
146
+ datagram = UDP.encode_datagram(group.to_s, body.to_s)
147
+ @socket.send(datagram, 0, @host, @port)
148
+ rescue Errno::ECONNREFUSED, Errno::ENETUNREACH
149
+ # UDP fire-and-forget — drop silently
150
+ end
151
+
152
+ alias send_message write_message
153
+
154
+ # No-op; UDP datagrams are sent immediately.
155
+ def flush
156
+ end
157
+
158
+
159
+ # Whether this connection uses CURVE encryption.
160
+ #
161
+ # @return [Boolean] always false
162
+ def curve?
163
+ false
164
+ end
165
+
166
+
167
+ # Closes the underlying UDP socket.
168
+ def close
169
+ @socket.close rescue nil
170
+ end
171
+ end
172
+
173
+
174
+ # Incoming UDP connection for DISH sockets.
175
+ #
176
+ # Tracks joined groups locally. JOIN/LEAVE commands from the
177
+ # DISH routing strategy are intercepted via #send_command and
178
+ # never transmitted on the wire.
179
+ #
180
+ class DishConnection
181
+ # @param socket [UDPSocket] bound socket
182
+ #
183
+ def initialize(socket)
184
+ @socket = socket
185
+ @groups = Set.new
186
+ end
187
+
188
+
189
+ # Receives one matching datagram, blocking until available.
190
+ #
191
+ # Async-safe: rescues IO::WaitReadable and yields to the
192
+ # fiber scheduler via #wait_readable.
193
+ #
194
+ # @return [Array<String>] [group, body], both binary-frozen
195
+ #
196
+ def receive_message
197
+ loop do
198
+ data, = @socket.recvfrom_nonblock(MAX_DATAGRAM_SIZE)
199
+ parts = UDP.decode_datagram(data)
200
+ next unless parts
201
+ group, body = parts
202
+ next unless @groups.include?(group.b)
203
+ return [group.b.freeze, body.b.freeze]
204
+ rescue IO::WaitReadable
205
+ @socket.wait_readable
206
+ retry
207
+ end
208
+ end
209
+
210
+
211
+ # Handles JOIN/LEAVE commands locally; nothing is sent on the wire.
212
+ #
213
+ # @param cmd [Protocol::ZMTP::Codec::Command]
214
+ #
215
+ def send_command(cmd)
216
+ case cmd.name
217
+ when "JOIN"
218
+ @groups << cmd.data.b.freeze
219
+ when "LEAVE"
220
+ @groups.delete(cmd.data.b)
221
+ end
222
+ end
223
+
224
+
225
+ # Whether this connection uses CURVE encryption.
226
+ #
227
+ # @return [Boolean] always false
228
+ def curve?
229
+ false
230
+ end
231
+
232
+
233
+ # Closes the underlying UDP socket.
234
+ def close
235
+ @socket.close rescue nil
236
+ end
237
+ end
238
+
239
+
240
+ # Bound UDP listener for DISH sockets.
241
+ #
242
+ # Unlike TCP/IPC listeners, there is no accept loop — a single
243
+ # DishConnection is registered directly with the engine, bypassing
244
+ # the ZMTP handshake path.
245
+ #
246
+ class Listener
247
+ # @return [String] bound endpoint
248
+ #
249
+ attr_reader :endpoint
250
+
251
+ # @param endpoint [String]
252
+ # @param socket [UDPSocket]
253
+ # @param engine [Engine]
254
+ #
255
+ def initialize(endpoint, socket, engine)
256
+ @endpoint = endpoint
257
+ @socket = socket
258
+ @engine = engine
259
+ end
260
+
261
+
262
+ # Registers a DishConnection with the engine.
263
+ # The on_accepted block is intentionally ignored — no ZMTP handshake.
264
+ #
265
+ # @param parent_task [Async::Task] (unused)
266
+ #
267
+ def start_accept_loops(parent_task, &_on_accepted)
268
+ conn = DishConnection.new(@socket)
269
+ @engine.connection_ready(conn, endpoint: @endpoint)
270
+ end
271
+
272
+
273
+ # Stops the listener.
274
+ #
275
+ def stop
276
+ @socket.close rescue nil
277
+ end
278
+ end
279
+ end
280
+ end
281
+ 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.22.1"
4
+ VERSION = "0.23.0"
5
5
  end
data/lib/omq.rb CHANGED
@@ -1,74 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # OMQ — pure Ruby ZeroMQ (ZMTP 3.1).
4
- #
5
- # Socket types live directly under OMQ:: for a clean API:
6
- # OMQ::PUSH, OMQ::PULL, OMQ::PUB, OMQ::SUB, etc.
7
- #
8
-
9
3
  require "protocol/zmtp"
10
4
  require "io/stream"
11
5
 
12
- require_relative "omq/version"
13
- require_relative "omq/monitor_event"
14
-
15
- module OMQ
16
- # When OMQ_DEBUG is set, silent rescue clauses print the exception
17
- # to stderr so transport/engine bugs surface immediately.
18
- DEBUG = !!ENV["OMQ_DEBUG"]
19
-
20
-
21
- # Raised when an internal pump task crashes unexpectedly.
22
- # The socket is no longer usable; the original error is available via #cause.
23
- #
24
- class SocketDeadError < RuntimeError
25
- end
26
-
27
-
28
- # Errors raised when a peer disconnects or resets the connection.
29
- # Not frozen at load time — transport plugins append to this before
30
- # the first bind/connect, which freezes both arrays.
31
- CONNECTION_LOST = [
32
- EOFError,
33
- IOError,
34
- Errno::EPIPE,
35
- Errno::ECONNRESET,
36
- Errno::ECONNABORTED,
37
- Errno::ENOTCONN,
38
- IO::Stream::ConnectionResetError,
39
- ]
40
-
41
-
42
- # Errors raised when a peer cannot be reached.
43
- CONNECTION_FAILED = [
44
- Errno::ECONNREFUSED,
45
- Errno::ENOENT,
46
- Errno::ETIMEDOUT,
47
- Errno::EHOSTUNREACH,
48
- Errno::ENETUNREACH,
49
- Errno::EPROTOTYPE, # IPC: existing socket file is SOCK_DGRAM, not SOCK_STREAM
50
- Socket::ResolutionError,
51
- ]
52
-
53
-
54
- # Freezes module-level state so OMQ sockets can be used inside Ractors.
55
- # Call this once before spawning any Ractors that create OMQ sockets.
56
- #
57
- def self.freeze_for_ractors!
58
- CONNECTION_LOST.freeze
59
- CONNECTION_FAILED.freeze
60
- Engine.transports.freeze
61
- Routing.registry.freeze
62
- end
63
- end
64
-
65
-
66
- # Transport
67
- require_relative "omq/transport/inproc"
68
- require_relative "omq/transport/tcp"
69
- require_relative "omq/transport/ipc"
70
6
 
71
7
  # Core
8
+ require_relative "omq/version"
9
+ require_relative "omq/constants"
72
10
  require_relative "omq/reactor"
73
11
  require_relative "omq/options"
74
12
  require_relative "omq/routing"
@@ -86,6 +24,13 @@ require_relative "omq/routing/xsub"
86
24
  require_relative "omq/routing/push"
87
25
  require_relative "omq/routing/pull"
88
26
  require_relative "omq/engine"
27
+
28
+ # Transport
29
+ require_relative "omq/transport/inproc"
30
+ require_relative "omq/transport/tcp"
31
+ require_relative "omq/transport/ipc"
32
+
33
+ # Mixins
89
34
  require_relative "omq/queue_interface"
90
35
  require_relative "omq/readable"
91
36
  require_relative "omq/writable"
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.22.1
4
+ version: 0.23.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrik Wenger
@@ -65,6 +65,9 @@ files:
65
65
  - LICENSE
66
66
  - README.md
67
67
  - lib/omq.rb
68
+ - lib/omq/channel.rb
69
+ - lib/omq/client_server.rb
70
+ - lib/omq/constants.rb
68
71
  - lib/omq/drop_queue.rb
69
72
  - lib/omq/engine.rb
70
73
  - lib/omq/engine/connection_lifecycle.rb
@@ -73,37 +76,48 @@ files:
73
76
  - lib/omq/engine/reconnect.rb
74
77
  - lib/omq/engine/recv_pump.rb
75
78
  - lib/omq/engine/socket_lifecycle.rb
76
- - lib/omq/monitor_event.rb
77
79
  - lib/omq/options.rb
78
80
  - lib/omq/pair.rb
81
+ - lib/omq/peer.rb
79
82
  - lib/omq/pub_sub.rb
80
83
  - lib/omq/push_pull.rb
81
84
  - lib/omq/queue_interface.rb
85
+ - lib/omq/radio_dish.rb
82
86
  - lib/omq/reactor.rb
83
87
  - lib/omq/readable.rb
84
88
  - lib/omq/req_rep.rb
85
89
  - lib/omq/router_dealer.rb
86
90
  - lib/omq/routing.rb
91
+ - lib/omq/routing/channel.rb
92
+ - lib/omq/routing/client.rb
87
93
  - lib/omq/routing/conn_send_pump.rb
88
94
  - lib/omq/routing/dealer.rb
95
+ - lib/omq/routing/dish.rb
89
96
  - lib/omq/routing/fan_out.rb
97
+ - lib/omq/routing/gather.rb
90
98
  - lib/omq/routing/pair.rb
99
+ - lib/omq/routing/peer.rb
91
100
  - lib/omq/routing/pub.rb
92
101
  - lib/omq/routing/pull.rb
93
102
  - lib/omq/routing/push.rb
103
+ - lib/omq/routing/radio.rb
94
104
  - lib/omq/routing/rep.rb
95
105
  - lib/omq/routing/req.rb
96
106
  - lib/omq/routing/round_robin.rb
97
107
  - lib/omq/routing/router.rb
108
+ - lib/omq/routing/scatter.rb
109
+ - lib/omq/routing/server.rb
98
110
  - lib/omq/routing/sub.rb
99
111
  - lib/omq/routing/xpub.rb
100
112
  - lib/omq/routing/xsub.rb
113
+ - lib/omq/scatter_gather.rb
101
114
  - lib/omq/single_frame.rb
102
115
  - lib/omq/socket.rb
103
116
  - lib/omq/transport/inproc.rb
104
117
  - lib/omq/transport/inproc/direct_pipe.rb
105
118
  - lib/omq/transport/ipc.rb
106
119
  - lib/omq/transport/tcp.rb
120
+ - lib/omq/transport/udp.rb
107
121
  - lib/omq/version.rb
108
122
  - lib/omq/writable.rb
109
123
  homepage: https://github.com/zeromq/omq
@@ -1,16 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OMQ
4
- # Lifecycle event emitted by {Socket#monitor}.
5
- #
6
- # @!attribute [r] type
7
- # @return [Symbol] event type (:listening, :connected, :disconnected, etc.)
8
- # @!attribute [r] endpoint
9
- # @return [String, nil] the endpoint involved
10
- # @!attribute [r] detail
11
- # @return [Hash, nil] extra context (e.g. { error: }, { interval: }, etc.)
12
- #
13
- MonitorEvent = Data.define(:type, :endpoint, :detail) do
14
- def initialize(type:, endpoint: nil, detail: nil) = super
15
- end
16
- end