fix: detect IPv6/dual-stack listeners in is_port_available (avoid silent 5432 collision)#20
Conversation
is_port_available() only probed 127.0.0.1, so a port held by an IPv6 or dual-stack listener (e.g. a Docker-published [::]:5432) looked free. The auto-allocation in start() then never triggered and pg0 silently bound the same port, colliding with the existing server (the connecting client then hits the wrong PostgreSQL). Treat a port as taken if binding either IPv4 or IPv6 loopback returns AddrInUse; ignore other bind errors (e.g. IPv6 disabled) so they don't make every port look unavailable. Refs vectorize-io/hindsight#2379 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR updates is_port_available() to correctly detect ports already occupied by IPv6 or dual-stack listeners (e.g. [::]:5432), preventing pg0 start from silently colliding with an existing Postgres that is not visible to an IPv4-only probe.
Changes:
- Expand the port probe to check both IPv4 (
127.0.0.1) and IPv6 (::1) loopback bindings. - Add an explanatory doc comment describing why IPv6/dual-stack listeners must be considered.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| matches!(result, Err(ref e) if e.kind() == std::io::ErrorKind::AddrInUse) | ||
| } | ||
| let v4 = std::net::TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, port)); | ||
| let v6 = std::net::TcpListener::bind((std::net::Ipv6Addr::LOCALHOST, port)); | ||
| !in_use(v4) && !in_use(v6) |
There was a problem hiding this comment.
Good catch — fixed in 60cd794. The IPv4 loopback bind is now required to succeed (any IPv4 error, including PermissionDenied on low ports like 80, returns unavailable), and the IPv6 probe is used solely to catch AddrInUse from an IPv6/dual-stack listener (other IPv6 errors like IPv6-disabled are ignored).
| /// port collision instead of auto-allocating a free port. Treat a port as taken | ||
| /// if binding either loopback family returns `AddrInUse`; ignore other errors | ||
| /// (such as IPv6 being disabled on the host) so they don't mark the port | ||
| /// unavailable. |
There was a problem hiding this comment.
Updated the doc comment in 60cd794 to state that the IPv4 loopback bind must succeed (pg0 connects over 127.0.0.1) and that only AddrInUse from the IPv6 probe is treated as taken — other IPv6 errors (e.g. IPv6 disabled) are ignored.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Update: installed the Rust toolchain and verified locally — |
Addresses review: pg0 connects over 127.0.0.1, so a port is only usable if the IPv4 loopback bind succeeds. Any IPv4 error (incl. PermissionDenied on low ports like 80) now means unavailable, instead of being treated as free. The IPv6 probe is used solely to catch AddrInUse from an IPv6/dual-stack listener; other IPv6 errors (e.g. IPv6 disabled) are ignored. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Closing as it's no longer needed vectorize-io/hindsight#2379 (comment) |
Problem
is_port_available()probes only IPv4 loopback:PostgreSQL with the default
listen_addresses = 'localhost'binds both127.0.0.1and::1. A port held by an IPv6 or dual-stack listener — e.g. a Docker-published[::]:5432— is invisible to an IPv4-only probe, sois_port_available(5432)returnstrue. The auto-allocation instart():therefore never triggers, and pg0 binds the already-used port. The result is a silent collision: a client connecting to
127.0.0.1:5432may reach the other PostgreSQL and fail auth, with no indication that two servers are fighting over the port.Repro
[::]:5432(e.g. a Docker container with-p 5432:5432, which binds both0.0.0.0and[::]).pg0 start(no--port) → it reports port 5432 instead of auto-allocating, colliding with the container.Fix
Treat a port as taken if binding either IPv4 or IPv6 loopback returns
AddrInUse; ignore other bind errors (e.g. IPv6 disabled on the host) so they don't make every port look unavailable. This makes the existingfind_available_portauto-allocation fire on real collisions — including IPv6/dual-stack ones — matching the documented "auto-allocates if the default port is in use" behavior.Scope is limited to this one pure-stdlib function;
find_available_portalready calls it, so it's covered too.Notes
cargo build/tests locally —build.rsbundles the PostgreSQL/pgvector/pgbouncer platform binaries and I don't have the toolchain set up here — so this relies on CI to verify the build. The change is isolated and uses onlystd. Happy to adjust naming/style or add a test if you'd like.Refs vectorize-io/hindsight#2379 (surfaced while running Hindsight local-embedded mode, where the embedded PG silently took a Docker-occupied 5432).
🤖 Generated with Claude Code