Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

connect_tcp() is inconsistent between Tokio and async-std #2821

Closed
abonander opened this issue Oct 16, 2023 · 6 comments · Fixed by #2822
Closed

connect_tcp() is inconsistent between Tokio and async-std #2821

abonander opened this issue Oct 16, 2023 · 6 comments · Fixed by #2822
Labels

Comments

@abonander
Copy link
Collaborator

abonander commented Oct 16, 2023

While triaging #2808 I realized that we have inconsistent behavior between Tokio and async-std when opening a TCP connection, in the case that a domain name resolves to multiple IP addresses:

The Tokio socket implementation will actually try them all in order as we just forward to TcpStream::connect(): https://github.com/launchbadge/sqlx/blob/main/sqlx-core/src/net/socket/mod.rs#L197

However, the async-std implementation is a little funky because async_std::net::TcpStream doesn't have the same set of calls we expect (try_read, try_write, poll_ready()) and so we have to go down one level and use async-io directly. But Async<TcpStream>::connect() doesn't use ToSocketAddrs so I had to implement that part manually, and I didn't realize the behavior as-written was inconsistent.

This is an easy fix, though:

let socket_addr = (host, port)
.to_socket_addrs()
.await?
.next()
.expect("BUG: to_socket_addrs() should have returned at least one result");

That code should iterate through the addresses returned by (host, port).to_socket_addrs().await? and try to connect to each one in turn, returning the first that succeeds or the last error otherwise. If it resolves to zero addresses, it should also return an error instead of panicking (what actually happens if the hostname doesn't resolve to anything? I don't actually know).

It'd be pretty complex to set up a regression test for it, though it'd be nice to at least manually test it.

@anupj
Copy link
Contributor

anupj commented Oct 17, 2023

@abonander so something like this would work?

    #[cfg(feature = "_rt-async-std")]
    {
        use async_io::Async;
        use async_std::net::ToSocketAddrs;
        use std::net::TcpStream;

        let mut last_err = None;

        // Loop through all the Socket Addresses that the hostname resolves to
        for socket_addr in (host, port).to_socket_addrs().await? {
            match Async::<TcpStream>::connect(socket_addr).await {
                Ok(stream) => return Ok(with_socket.with_socket(stream)),
                Err(e) => last_err = Some(e),
            }
        }

        // If we reach this point, it means we failed to connect to any of the addresses.
        // Return the last error we encountered, or a custom error if the hostname didn't resolve to any address.
        match last_err {
            Some(err) => return Err(err.into()),
            None => {
                return Err(io::Error::new(
                    io::ErrorKind::AddrNotAvailable,
                    "Hostname did not resolve to any addresses",
                )
                .into())
            }
        }
    }

I tried this on my fork but the tests fail with the following (unrelated) message:

thread 'it_does_not_stop_stream_after_decoding_error' panicked at 'at least one of the `runtime-*` features must be enabled', tests/any/any.rs:47:1

@abonander
Copy link
Collaborator Author

abonander commented Oct 17, 2023

@anupj you have to enable the runtime-async-std feature.

@anupj
Copy link
Contributor

anupj commented Oct 17, 2023

@abonander I am now following the tests README.md instructions and I get this error message when I run ./x.py.

failures:

---- it_opens_with_extension stdout ----
Error: error returned from database: (code: 0) dlopen(ipaddr.dylib, 0x000A): tried: 'ipaddr.dylib' (no such file), '/System/Volumes/Preboot/Cryptexes/OSipaddr.dylib' (no such file), '/usr/lib/ipaddr.dylib' (no such file, not in dyld cache), 'ipaddr.dylib'

anupj added a commit to anupj/sqlx that referenced this issue Oct 18, 2023
… connection

This commit updates the error handling logic in the `connect_tcp` function. Previously, the function would panic if the hostname did not resolve to at least one address.

The updated logic attempts to establish a TCP connection for each address that the hostname resolves to. If it fails to connect to any of the addresses, it will return the last encountered error. If the hostname doesn't resolve to any addresses, the function returns a custom error message stating "Hostname did not resolve to any addresses".
@abonander
Copy link
Collaborator Author

There's some fixes to x.py in #2754 and it looks like that error is one of them. I've reopened it to merge.

@abonander
Copy link
Collaborator Author

If you rebase, x.py should be working now.

abonander pushed a commit that referenced this issue Oct 19, 2023
…#2822)

This commit updates the error handling logic in the `connect_tcp` function. Previously, the function would panic if the hostname did not resolve to at least one address.

The updated logic attempts to establish a TCP connection for each address that the hostname resolves to. If it fails to connect to any of the addresses, it will return the last encountered error. If the hostname doesn't resolve to any addresses, the function returns a custom error message stating "Hostname did not resolve to any addresses".
@anupj
Copy link
Contributor

anupj commented Oct 20, 2023

Thank you @abonander and thanks for merging my PR 👍🏽

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants