omq 0.6.3 → 0.6.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6a43ed94bf4595d9388897a74f6e642a865ec98a66c0cf64db084d6cb714705e
4
- data.tar.gz: feb63595995ac894ea816e44f9ebf01d3bacf756cdb8c55b6f5438f00a6aab96
3
+ metadata.gz: 94402697e206444e39655869c1382cea2a2839732d373d6cbcc237dcb125cd44
4
+ data.tar.gz: dbcaa10eb1213e935325d0290611716552d28394dd479d8fc79564a0af6f0e4d
5
5
  SHA512:
6
- metadata.gz: 9bbe45d76566342849dcc59a8a334cf98c4f79b56392c0031e2491d4a0d3fb45d9ce7c0a9235c7138f2dd483b40721baa7fd9ce3d189c3230757f30a9c9e57f4
7
- data.tar.gz: 1b7eccb931b57458c6cec4321fa3b4ee1de34314de7855aa3772affc434fb32561cf18e4365ab17ef5af6d5dbad62e24e7eba57d1e166d279a2b519fcfbfcf0f
6
+ metadata.gz: 9a4b4eab9767b52f874bf85816589558bf42e9730da827220490e80ca9dd499ae5ae604c06022d3db07402042a1b52ea1c88737e2c1556b4d78af90391dc1d2f
7
+ data.tar.gz: e2db5e6aaea16af48024be90af8353a81f4d77a4b0b1973131f74dbd915b9aaf52fdd06d08cc57749ffb74b6bd70214ffb2ab04063925887c8a1b72afd4582cb
data/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.5 — 2026-03-30
4
+
5
+ ### Fixed
6
+
7
+ - **CLI error path** — use `Kernel#exit` instead of `Process.exit!`
8
+
9
+ ## 0.6.4 — 2026-03-30
10
+
11
+ ### Added
12
+
13
+ - **Dual-stack TCP bind** — `TCP.bind` resolves the hostname via
14
+ `Addrinfo.getaddrinfo` and binds to all returned addresses.
15
+ `tcp://localhost:PORT` now listens on both `127.0.0.1` and `::1`.
16
+ - **Eager DNS validation on connect** — `Engine#connect` resolves TCP
17
+ hostnames upfront via `Addrinfo.getaddrinfo`. Unresolvable hostnames
18
+ raise `Socket::ResolutionError` immediately instead of failing silently
19
+ in the background reconnect loop.
20
+ - **`Socket::ResolutionError` in `CONNECTION_FAILED`** — DNS failures
21
+ during reconnect are now retried with backoff (DNS may recover or
22
+ change), matching libzmq behavior.
23
+ - **CLI catches `SocketDeadError` and `Socket::ResolutionError`** —
24
+ prints the error and exits with code 1 instead of silently exiting 0.
25
+
26
+ ### Improved
27
+
28
+ - **CLI endpoint shorthand** — `tcp://:PORT` expands to
29
+ `tcp://localhost:PORT` (loopback, safe default). `tcp://*:PORT` expands
30
+ to `tcp://0.0.0.0:PORT` (all interfaces, explicit opt-in).
31
+
32
+ ### Fixed
33
+
34
+ - **`tcp://*:PORT` failed on macOS** — `*` is not a resolvable hostname.
35
+ Connects now use `localhost` by default; `*` only expands to `0.0.0.0`
36
+ for explicit all-interface binding.
37
+ - **`Socket` constant resolution inside `OMQ` namespace** — bare `Socket`
38
+ resolved to `OMQ::Socket` instead of `::Socket`, causing `NameError`
39
+ for `Socket::ResolutionError` and `Socket::AI_PASSIVE`.
40
+
3
41
  ## 0.6.3 — 2026-03-30
4
42
 
5
43
  ### Fixed
data/README.md CHANGED
@@ -107,18 +107,18 @@ end
107
107
 
108
108
  ## Socket Types
109
109
 
110
- | Pattern | Classes | Direction |
111
- |---------|---------|-----------|
112
- | Request/Reply | `REQ`, `REP` | bidirectional |
113
- | Publish/Subscribe | `PUB`, `SUB`, `XPUB`, `XSUB` | unidirectional |
114
- | Pipeline | `PUSH`, `PULL` | unidirectional |
115
- | Routing | `DEALER`, `ROUTER` | bidirectional |
116
- | Exclusive pair | `PAIR` | bidirectional |
117
- | Client/Server | `CLIENT`, `SERVER` | bidirectional |
118
- | Group messaging | `RADIO`, `DISH` | unidirectional |
119
- | Pipeline (draft) | `SCATTER`, `GATHER` | unidirectional |
120
- | Peer-to-peer | `PEER` | bidirectional |
121
- | Channel (draft) | `CHANNEL` | bidirectional |
110
+ | Pattern | Classes | Direction | Multipart |
111
+ |---------|---------|-----------|-----------|
112
+ | Request/Reply | `REQ`, `REP` | bidirectional | yes |
113
+ | Publish/Subscribe | `PUB`, `SUB`, `XPUB`, `XSUB` | unidirectional | yes |
114
+ | Pipeline | `PUSH`, `PULL` | unidirectional | yes |
115
+ | Routing | `DEALER`, `ROUTER` | bidirectional | yes |
116
+ | Exclusive pair | `PAIR` | bidirectional | yes |
117
+ | Client/Server | `CLIENT`, `SERVER` | bidirectional | no |
118
+ | Group messaging | `RADIO`, `DISH` | unidirectional | no |
119
+ | Pipeline (draft) | `SCATTER`, `GATHER` | unidirectional | no |
120
+ | Peer-to-peer | `PEER` | bidirectional | no |
121
+ | Channel (draft) | `CHANNEL` | bidirectional | no |
122
122
 
123
123
  All classes live under `OMQ::`. For the purists, `ØMQ` is an alias:
124
124
 
@@ -163,16 +163,16 @@ echo "hello" | omq req -c tcp://localhost:5555
163
163
  omq sub -b tcp://:5556 -s "weather." &
164
164
  echo "weather.nyc 72F" | omq pub -c tcp://localhost:5556 -d 0.3
165
165
 
166
- # Pipeline with filtering
166
+ # Pipeline with filtering ($F = message parts, $_ = first part)
167
+ # /regexp/ matches against $_, next skips, break stops
167
168
  tail -f /var/log/syslog | omq push -c tcp://collector:5557
168
- omq pull -b tcp://:5557 -e '$F.first.include?("error") ? $F : nil'
169
+ omq pull -b tcp://:5557 -e 'next unless /error/; $F'
169
170
 
170
171
  # Pipe: PULL → eval → PUSH in one process
171
172
  omq pipe -c ipc://@work -c ipc://@sink -e '$F.map(&:upcase)'
172
173
 
173
- # Pipe with 4 Ractor workers for CPU parallelism
174
- omq pipe -c ipc://@work -c ipc://@sink -P 4 \
175
- -r ./fib.rb -e 'fib(Integer($_)).to_s'
174
+ # Pipe with Ractor workers for CPU parallelism (-P = all CPUs)
175
+ omq pipe -c ipc://@work -c ipc://@sink -P -r./fib -e 'fib(Integer($_)).to_s'
176
176
 
177
177
  # Exit when all peers disconnect (pipeline workers, sinks)
178
178
  omq pipe -c ipc://@work -c ipc://@sink --transient -e '$F'
@@ -412,7 +412,7 @@ module OMQ
412
412
  end
413
413
  rescue => e
414
414
  $stderr.puts "omq: -e error: #{e.message} (#{e.class})"
415
- Process.exit!(3)
415
+ exit 3
416
416
  end
417
417
 
418
418
  # ── Logging ─────────────────────────────────────────────────────
data/lib/omq/cli/pipe.rb CHANGED
@@ -237,7 +237,7 @@ module OMQ
237
237
  end
238
238
  rescue => e
239
239
  $stderr.puts "omq: -e error: #{e.message} (#{e.class})"
240
- Process.exit!(3)
240
+ exit 3
241
241
  end
242
242
 
243
243
 
data/lib/omq/cli.rb CHANGED
@@ -268,6 +268,12 @@ module OMQ
268
268
  rescue IO::TimeoutError, Async::TimeoutError
269
269
  $stderr.puts "omq: timeout" unless config.quiet
270
270
  exit 2
271
+ rescue OMQ::SocketDeadError => e
272
+ $stderr.puts "omq: #{e.cause.class}: #{e.cause.message}"
273
+ exit 1
274
+ rescue ::Socket::ResolutionError => e
275
+ $stderr.puts "omq: #{e.message}"
276
+ exit 1
271
277
  end
272
278
  end
273
279
 
@@ -415,9 +421,9 @@ module OMQ
415
421
 
416
422
  opts[:type_name] = type_name.downcase
417
423
 
418
- normalize = ->(url) { url.sub(%r{\Atcp://:}, "tcp://*:") }
419
- opts[:connects].map!(&normalize)
424
+ normalize = ->(url) { url.sub(%r{\Atcp://\*:}, "tcp://0.0.0.0:").sub(%r{\Atcp://:}, "tcp://localhost:") }
420
425
  opts[:binds].map!(&normalize)
426
+ opts[:connects].map!(&normalize)
421
427
  opts[:endpoints].map! { |ep| Endpoint.new(normalize.call(ep.url), ep.bind?) }
422
428
 
423
429
  opts
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.6.3"
4
+ VERSION = "0.6.5"
5
5
  end
@@ -89,6 +89,7 @@ module OMQ
89
89
  #
90
90
  def connect(endpoint)
91
91
  capture_parent_task
92
+ validate_endpoint!(endpoint)
92
93
  @connected_endpoints << endpoint
93
94
  if endpoint.start_with?("inproc://")
94
95
  # Inproc connect is synchronous and instant
@@ -348,6 +349,7 @@ module OMQ
348
349
  wrapped
349
350
  end
350
351
  @routing.recv_queue.enqueue(nil) rescue nil
352
+ @peer_connected.resolve(nil) rescue nil
351
353
  end
352
354
 
353
355
 
@@ -462,10 +464,26 @@ module OMQ
462
464
  delay = ri.is_a?(Range) ? ri.begin : ri if delay == 0
463
465
  end
464
466
  end
467
+ rescue Async::Stop
468
+ # normal shutdown
469
+ rescue => error
470
+ signal_fatal_error(error)
465
471
  end
466
472
  end
467
473
 
468
474
 
475
+ # Eagerly validates TCP hostnames so resolution errors fail
476
+ # on connect, not silently in the background reconnect loop.
477
+ # Reconnects still re-resolve (DNS may change), and transient
478
+ # resolution failures during reconnect are retried with backoff.
479
+ #
480
+ def validate_endpoint!(endpoint)
481
+ return unless endpoint.start_with?("tcp://")
482
+ host = URI.parse(endpoint.sub("tcp://", "http://")).hostname
483
+ Addrinfo.getaddrinfo(host, nil, nil, :STREAM) if host
484
+ end
485
+
486
+
469
487
  def transport_for(endpoint)
470
488
  case endpoint
471
489
  when /\Atcp:\/\// then Transport::TCP
@@ -20,23 +20,38 @@ module OMQ
20
20
  def bind(endpoint, engine)
21
21
  host, port = parse_endpoint(endpoint)
22
22
  host = "0.0.0.0" if host == "*"
23
- server = TCPServer.new(host, port)
24
- actual_port = server.local_address.ip_port
25
- host_part = host.include?(":") ? "[#{host}]" : host
26
- resolved = "tcp://#{host_part}:#{actual_port}"
27
-
28
- accept_task = Reactor.spawn_pump(annotation: "tcp accept #{resolved}") do
29
- loop do
30
- client = server.accept
31
- Async::Task.current.defer_stop do
32
- engine.handle_accepted(IO::Stream::Buffered.wrap(client), endpoint: resolved)
23
+
24
+ addrs = Addrinfo.getaddrinfo(host, port, nil, :STREAM, nil, ::Socket::AI_PASSIVE)
25
+ raise ::Socket::ResolutionError, "no addresses for #{host}" if addrs.empty?
26
+
27
+ servers = []
28
+ accept_tasks = []
29
+ actual_port = nil
30
+
31
+ addrs.each do |addr|
32
+ server = TCPServer.new(addr.ip_address, actual_port || port)
33
+ actual_port ||= server.local_address.ip_port
34
+ servers << server
35
+
36
+ ip = addr.ip_address
37
+ host_part = ip.include?(":") ? "[#{ip}]" : ip
38
+ resolved = "tcp://#{host_part}:#{actual_port}"
39
+
40
+ accept_tasks << Reactor.spawn_pump(annotation: "tcp accept #{resolved}") do
41
+ loop do
42
+ client = server.accept
43
+ Async::Task.current.defer_stop do
44
+ engine.handle_accepted(IO::Stream::Buffered.wrap(client), endpoint: resolved)
45
+ end
33
46
  end
47
+ rescue IOError
48
+ # server closed
34
49
  end
35
- rescue IOError
36
- # server closed
37
50
  end
38
51
 
39
- Listener.new(resolved, server, accept_task, actual_port)
52
+ host_part = host.include?(":") ? "[#{host}]" : host
53
+ resolved = "tcp://#{host_part}:#{actual_port}"
54
+ Listener.new(resolved, servers, accept_tasks, actual_port)
40
55
  end
41
56
 
42
57
  # Connects to a TCP endpoint.
@@ -81,19 +96,19 @@ module OMQ
81
96
  # @param accept_task [#stop] the accept loop handle
82
97
  # @param port [Integer] bound port number
83
98
  #
84
- def initialize(endpoint, server, accept_task, port)
85
- @endpoint = endpoint
86
- @server = server
87
- @accept_task = accept_task
88
- @port = port
99
+ def initialize(endpoint, servers, accept_tasks, port)
100
+ @endpoint = endpoint
101
+ @servers = servers
102
+ @accept_tasks = accept_tasks
103
+ @port = port
89
104
  end
90
105
 
91
106
 
92
107
  # Stops the listener.
93
108
  #
94
109
  def stop
95
- @accept_task.stop
96
- @server.close rescue nil
110
+ @accept_tasks.each(&:stop)
111
+ @servers.each { |s| s.close rescue nil }
97
112
  end
98
113
  end
99
114
  end
data/lib/omq/zmtp.rb CHANGED
@@ -27,6 +27,7 @@ module OMQ
27
27
  Errno::ETIMEDOUT,
28
28
  Errno::EHOSTUNREACH,
29
29
  Errno::ENETUNREACH,
30
+ Socket::ResolutionError,
30
31
  ].freeze
31
32
  end
32
33
  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.6.3
4
+ version: 0.6.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrik Wenger