omq 0.6.3 → 0.6.4

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: 77a41ed459e51dc42f2dcec6976284b7fcbacc2b2a5e946234b76fbf93af74c0
4
+ data.tar.gz: b12af7e7377465ee799c9ff213b2d1b1604bec635fd9d48bfb7af8112fb6a4a9
5
5
  SHA512:
6
- metadata.gz: 9bbe45d76566342849dcc59a8a334cf98c4f79b56392c0031e2491d4a0d3fb45d9ce7c0a9235c7138f2dd483b40721baa7fd9ce3d189c3230757f30a9c9e57f4
7
- data.tar.gz: 1b7eccb931b57458c6cec4321fa3b4ee1de34314de7855aa3772affc434fb32561cf18e4365ab17ef5af6d5dbad62e24e7eba57d1e166d279a2b519fcfbfcf0f
6
+ metadata.gz: 44b729c9f970e4fd1c3203dad3cde213464d00d010513d983ca019a577fc123953cc5b6de520b64a821e8fabcfb7778db3a50876416ac19e5ee3b0082804af37
7
+ data.tar.gz: a83241f60938d62edf6241ab6de7719594689cfaa551988d852a39043d297464b7f64405d661ea3c7fdd3d198143f6f263fa930c48490cd73cf45547d566c3f2
data/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.4 — 2026-03-30
4
+
5
+ ### Added
6
+
7
+ - **Dual-stack TCP bind** — `TCP.bind` resolves the hostname via
8
+ `Addrinfo.getaddrinfo` and binds to all returned addresses.
9
+ `tcp://localhost:PORT` now listens on both `127.0.0.1` and `::1`.
10
+ - **Eager DNS validation on connect** — `Engine#connect` resolves TCP
11
+ hostnames upfront via `Addrinfo.getaddrinfo`. Unresolvable hostnames
12
+ raise `Socket::ResolutionError` immediately instead of failing silently
13
+ in the background reconnect loop.
14
+ - **`Socket::ResolutionError` in `CONNECTION_FAILED`** — DNS failures
15
+ during reconnect are now retried with backoff (DNS may recover or
16
+ change), matching libzmq behavior.
17
+ - **CLI catches `SocketDeadError` and `Socket::ResolutionError`** —
18
+ prints the error and exits with code 1 instead of silently exiting 0.
19
+
20
+ ### Improved
21
+
22
+ - **CLI endpoint shorthand** — `tcp://:PORT` expands to
23
+ `tcp://localhost:PORT` (loopback, safe default). `tcp://*:PORT` expands
24
+ to `tcp://0.0.0.0:PORT` (all interfaces, explicit opt-in).
25
+
26
+ ### Fixed
27
+
28
+ - **`tcp://*:PORT` failed on macOS** — `*` is not a resolvable hostname.
29
+ Connects now use `localhost` by default; `*` only expands to `0.0.0.0`
30
+ for explicit all-interface binding.
31
+ - **`Socket` constant resolution inside `OMQ` namespace** — bare `Socket`
32
+ resolved to `OMQ::Socket` instead of `::Socket`, causing `NameError`
33
+ for `Socket::ResolutionError` and `Socket::AI_PASSIVE`.
34
+
3
35
  ## 0.6.3 — 2026-03-30
4
36
 
5
37
  ### Fixed
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.4"
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.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrik Wenger