-
Notifications
You must be signed in to change notification settings - Fork 5.1k
valkey: reject connect() and call onclose with the real failure reason #39569
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
Closed
Closed
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
8d1ea20
redis: report the real failure reason to connect() and onclose
alii e8dba28
test: accept the errno message in the container-gated valkey tests
alii 5a80b78
share the connect errno normalisation with the redis client
alii e99cc42
valkey: take the connect error by reference
alii 44dfadb
usockets: restore the connect errno after the failed dial's socket is…
robobun ef6741a
valkey: test that a failed lookup rejects connect() with the resolver…
robobun File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Calling
close()while anInternalSocket::Connectingis in flight (async DNS / happy-eyeballs) now surfacesconnect ECONNREFUSED <host>:<port>instead of the previous neutralConnection closed, becauseus_connecting_socket_closesynthesisesECONNABORTEDandconnect_errnomaps that toECONNREFUSED. Nothing was refused — the user aborted; consider checkingis_manually_closed(or special-casingECONNABORTED) inon_connect_errorand passingCloseReason::SocketClosedin that case. Nit: error code staysERR_REDIS_CONNECTION_CLOSEDand the window is narrow.Extended reasoning...
What the bug is
When a user calls
client.close()while the client's socket is anInternalSocket::Connecting(a hostname that did not parse as an IP literal and had no single cached DNS result — i.e. async DNS or happy-eyeballs is in flight), theconnect()promise andonclosenow receive the messageconnect ECONNREFUSED <host>:<port>. Before this PR they received the neutralConnection closed. Nothing was refused; the user aborted the dial themselves.The specific code path
Step-by-step:
disconnect()→ValkeyClient::close(FastShutdown)→AnySocket::close→ConnectingSocket::close→us_connecting_socket_close(c).us_connecting_socket_close(socket.c:215-218):if (!c->error) c->error = ECONNABORTED;, then dispatchesus_dispatch_connecting_error(c, c->error).SocketHandler::on_connect_error(this, from_connecting(c), ECONNABORTED).socket.dns_error()readsc->error_is_dns ? c->error : 0;error_is_dnswas never set for a manual abort → returns0.on_connect_erroris skipped (dns_error == 0), so it falls toJSValkeyClient::connect_error_message(address, ECONNABORTED).bun_errno::connect_errno(ECONNABORTED):ECONNABORTEDis not in theKEPTlist (ENOENT,ENOTSOCK,EACCES,EINVAL,ECONNRESET,EADDRINUSE,EADDRNOTAVAIL) → returnsSystemErrno::ECONNREFUSED.b"connect ECONNREFUSED <host>:<port>";on_close(CloseReason::DialFailed(&message))is called.ValkeyClient::on_close,is_manually_closed == trueandfailure.is_none(), so it takes the first branch and callsself.fail(message, ConnectionClosed), which records that string asself.failure.on_valkey_closerejects the pendingconnect()promise and callsonclosewith that recorded error.Why existing code doesn't prevent it
Before this PR,
on_connect_errorignoredcodeentirely and calledon_close()with no argument, which unconditionally usedb"Connection closed". Nowon_closetakes the reason from the caller, andon_connect_errorbuilds one from the errno without distinguishing a caller-initiated abort from a dial that actually failed.The other close-while-connecting shapes are unaffected: a
Connectedsemi-socket goes through theis_semi_socketbranch ofValkeyClient::close(CloseReason::SocketClosed), and afail()-initiated close of aConnectingsocket already hasfailure.is_some(), so the secondfail()insideon_closeis a no-op andon_valkey_closereports the previously-recorded failure. Only the manualdisconnect()whileInternalSocket::Connectingregresses.Concrete example
Impact
Message-accuracy regression only. The error code stays
ERR_REDIS_CONNECTION_CLOSED, the trigger window is narrow (manualclose()during the brief async-DNS/happy-eyeballs window; hostname must not be an IP literal and not in the DNS cache), and the user calledclose()themselves so they already know why the connection ended. No functional breakage. Still worth noting since the PR's stated purpose is accurate failure messages, and per REVIEW.md "error messages are reviewed word-for-word as code."How to fix
Either check
this.client.get().flags.is_manually_closedinon_connect_errorand passCloseReason::SocketClosedin that case, or special-casecode == ECONNABORTED(which uSockets uses specifically as its "caller aborted" sentinel) toCloseReason::SocketClosed. The former is more direct sinceis_manually_closedis exactly the signal that the user initiated this.