Skip to content

feat(xpp): async DNS + hostname-aware TCP/UDP - #204

Merged
mivinci merged 4 commits into
mainfrom
claude/async-dns-and-tcp-hostname
May 30, 2026
Merged

mivinci merged 4 commits into
mainfrom
claude/async-dns-and-tcp-hostname

Conversation

@mivinci

@mivinci mivinci commented May 29, 2026

Copy link
Copy Markdown
Owner

Summary

  • New xpp::net::lookup_host("host:port") — wraps getaddrinfo in spawn_blocking, mirrors tokio::net::lookup_host. Accepts IPv4/IPv6 literals and hostnames in one string.
  • TcpListener::bind / TcpStream::connect and UdpSocket::bind / UdpSocket::connect take a "host:port" string and resolve via DNS, trying each address in order. Synchronous fast paths move to bind_addr / connect_addr.
  • Underpins these: Runtime::spawn_blocking + a Tokio-style 512-thread blocking-pool headroom on xTaskGroup to prevent dead-lock when async workers occupy every slot. BlockingSpawnTask keeps allocations minimal (2 heap allocs per call instead of 4).
  • AdapterPromiseNode::m_val now stores Option<T> so non-default-constructible payloads (Result<T,E>) can flow through Promise chains.

Error model

All connect/bind/accept paths return Promise<Result<…, SocketError>>. The empty-stream sentinel that connect/accept previously used to signal failure is gone. New error variants: ResolveFailed, NoAddress, Closed.

Test plan

  • build/xpp_test: 649 pass, 0 fail
  • build-cxx20/xpp_test: 680 pass, 0 fail
  • build-cxx20/xpp_coro_test: 31 pass, 0 fail
  • New: DnsTest (8 cases: IPv4/IPv6 literals, localhost, bogus hostname, malformed inputs, blocking-pool concurrency)
  • New: TcpHostnameTest (9 cases: bind/connect by host:port, IPv6, accept Result, closed-listener)
  • New: UdpSocketTest BindHostPort* / ConnectHostPort (4 cases)
  • New: RuntimeTest.SpawnBlocking* (7 cases incl. DoesNotStarveWorkers — regression test for the dead-lock fix)
  • New: RuntimeTest.SpawnBlockingAwait* coroutine variants (7 cases)
  • All preexisting tests still pass (no regressions)

Notable behavior changes

  • TcpStream::connect(SocketAddr) is now connect_addr(SocketAddr) -> Promise<Result<TcpStream, SocketError>>. The free connect name is reserved for the hostname overload.
  • TcpListener::bind(SocketAddr) → bind_addr(SocketAddr). Same renaming for UdpSocket.
  • TcpListener::accept() returns Promise<Result<TcpStream, SocketError>> instead of Promise<TcpStream>.

🤖 Generated with Claude Code

mivinci and others added 4 commits May 30, 2026 00:35
Three layered changes:

1. spawn_blocking + blocking pool fix.  worker_main now consumes the
   N async threads, while xTaskGroup has +512 headroom (matches
   Tokio's thread_cap default) so blocking work expands the pool on
   demand instead of dead-locking.  Inlines the adapter into a single
   BlockingSpawnTask, cutting per-call allocations from 4 to 2.

2. Async DNS via lookup_host("host:port").  Wraps getaddrinfo in
   spawn_blocking — same pattern as tokio::net::lookup_host.  Accepts
   IPv4 / IPv6 / hostname literals.

3. TcpListener::bind / TcpStream::connect take a "host:port" string
   and resolve via DNS, trying each resolved address in order.
   The pre-parsed-SocketAddr fast path is renamed to bind_addr /
   connect_addr.  Both return Promise<Result<…, SocketError>>.

Also: AdapterPromiseNode::m_val now wraps T in Option so non-default-
constructible payloads (like Result<T,E>) can flow through Promise
chains; SocketError gains ResolveFailed and NoAddress.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-up cleanup on the async DNS / TCP work:

- TcpStream::connect_addr and TcpListener::accept now return
  Promise<Result<…, SocketError>>, replacing the empty-stream
  sentinel.  connect_each in the hostname path becomes a clean
  Result chain.
- UdpSocket::bind / connect get matching "host:port" overloads;
  the synchronous SocketAddr fast-paths move to bind_addr /
  connect_addr (parallel to TCP).
- New SocketError::Closed for operations on closed sockets
  (set_nodelay, nodelay, accept on a closed listener).
- SocketAddr::unspecified() helper replaces five copies of
  SocketAddr::from(SocketAddrV4::from(UNSPECIFIED, 0)).
- lookup_host's host buffer is NI_MAXHOST instead of 256, so
  any RFC-compliant DNS name (max 253) fits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Linux fast path declared `int fd = ::socket(...)` inside
`#ifdef __linux__`, then the fallback path re-declared `int fd`
unconditionally — legal on macOS (the first decl is preprocessed
out) but a redeclaration error on Linux.

Move the single declaration above the #ifdef.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous version stashed N concurrent lookup_host promises in a
Vec, then awaited only the last and let the others detach.  On
single-core Linux CI runners the test hung past the 60 s ctest
timeout — the detached-promise + main-loop done_fn timing has a
genuine ordering issue worth investigating, but the test itself is
not the right place to assert blocking-pool concurrency.

Switch to four sequential lookups.  Each is a separate
spawn_blocking submission, which is what we actually wanted to
exercise; concurrent variants are covered by
RuntimeTest.SpawnBlockingDoesNotStarveWorkers (coroutine path).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mivinci
mivinci merged commit 5b59b58 into main May 30, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant