-
Notifications
You must be signed in to change notification settings - Fork 30k
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
http: add reusedSocket property on client request #29715
Conversation
Not addressing the merits of the change yet, but this would need docs and unit tests. Also, can you expand on why this is helpful |
@jasnell Thanks for replying, I'm not sure if it's a good solution. Let me elaborate the problem first: When doing http RPC, we normally keep the connection alive, but it's often when server closed a connection due to idle timeout, which is 5s by default, client send a request before receiving the closed event, then it would end in ECONNRESET err. Here's an example to reproduce that. const http = require("http");
const agent = new http.Agent({ keepAlive: true });
// server has a 5 seconds default idle timeout
http
.createServer((req, res) => {
res.write("foo\n");
res.end();
})
.listen(3000);
setInterval(() => {
// using a keep-alive agent
http.get("http://localhost:3000", { agent }, res => {
res.on("data", data => {
//ignore
});
res.on("end", () => {
console.log("success");
});
})
}, 5000); // sending request on 5s interval so it's easy to hit idle timeout Wait for about 20s you will get an A possible solution is retry on such kind of unexpected connection close. But we need to determine it's a reused socket closing, or a real server reject(maybe server is not running at all). So here I add a The simplified retry checking: const req = http
.get("http://localhost:3000", { agent }, res => {
// ...
})
.on("error", err => {
if (req.reusedSocket && err.code === "ECONNRESET") { // check if retry is needed
// retry
}
}); |
@nodejs/http ... thoughts? |
@nodejs/collaborators |
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.
Would you mind adding some tests for this?
I would prefer if we tracked the “closing” state in the Agent, and avoid sending a request on a socket we are closing. |
@mcollina No problem, I'll add some tests and docs.
We already have closing handling in Agent, but the problem is client could send out request in the time between server socket closing and client receiving closing event. So retry is inevitable in this situation. Event sequence would be like:
|
doc/api/http.md
Outdated
### request.reusedSocket | ||
|
||
<!-- YAML | ||
added: v12.12.0 |
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.
I'm not sure what version should be put here.
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.
It should be added: REPLACEME
. The next time a release is made, tooling will replace it with the correct version number.
@nodejs/http This could use some reviews. |
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.
LGTM
Shall I do something to make CI pass? @mcollina |
This is a bit weird to me. Eventually you will end up with every socket reused. So what’s the difference between that state and just having the flag true on every socket from the beginning?
|
}) | ||
.on('error', (err) => { | ||
// Check if retry is needed | ||
if (req.reusedSocket && err.code === 'ECONNRESET') { |
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.
This could retry infinitely depending on what causes the econnreset?
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.
Can probably be resolved by retrying without keep alive agent and only applying this logic in the keep alive case.
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.
It wont be infinitely retrying,the client would finally received socket closes event and remove the broken socket,next connection ‘s first request would be non-reused socket
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.
but the next socket could have the same problem? But yea, I guess it would run out of pooled sockets eventually.
}) | ||
.on('error', (err) => { | ||
// Check if retry is needed | ||
if (req.reusedSocket && err.code === 'ECONNRESET') { |
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.
Isn't req.agent.keepAlive
enough here, instead of req.reusedSocket
?
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.
The difference is the first request of a keep-alive agent is based on a new created socket, which is not reused. If the first request failed with ECONNRESET
, we would know the server is not listening, there's no need to retry.
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.
I don't think it hurts to retry in that edge case but yes, I can't think of a good way to accomplish avoiding the retry without the property you suggest.
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.
You are right. Disregard my comment.
I think this achieves what this PR intends without modifying core: const agent = new http.Agent({ keepAlive: true });
function retriableRequest(url) {
return new Promise((resolve, reject) => http
.get(url, { agent }, resolve)
.on('error', (err) => {
// Check if retry is needed.
// Retry if:
// - Connection was reset.
// - We have not received a response.
// - Socket might have been re-used through keep alive agent.
if (err.code === 'ECONNRESET' && !req.res && req.agent.keepAlive) {
// Retry without keep alive to avoid infinite loop.
http
.get(url, { agent: null }, resolve)
.on('error', reject);
} else {
reject(err);
}
}
);
} There is one case that neither this PR or my suggestion addresses... what if it was a non idempotent request and the server actually did receive and act on it before e.g. async function handle(req, res) {
if (req.method === 'POST') {
await createResource(req);
// everything fine so far
// oops... server crash or various other reasons that might cause a ECONNRESET
}
} Which begs the question. Should keepAlive be used for non-idempotent methods? |
@ronag I agree with you on the idempotent point, it's not totally safe to retry on an For example, the popular request library thought it's OK in practical to retry on Or user can just retry on
Which is better but not totally safe also. |
Node http collaborators. This is kind of a data race in regards to keep alive connections. Does the spec say anything about this? The way it is now it seems like the server should never close any socket that might be in a client's agent pool. |
|
Related:
Chromium also seem to have some sort of socketReused flag, https://codereview.chromium.org/303443011/patch/20001/30002. |
// ... | ||
}) | ||
.on('error', (err) => { | ||
// Check if retry is needed |
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.
Should probably include a note about idempotent methods.
}) | ||
.on('error', (err) => { | ||
// Check if retry is needed | ||
if (req.reusedSocket && err.code === 'ECONNRESET') { |
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.
I think this example should at least also check for !req.res
and not retry without keep alive in case the ECONNRESET
is caused by something else.
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.
I don't think !req.res
would be non-nil when got an ECONNRESET
error, and for a non-keep-alive agent, req.reusedSocket
would never be true.
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.
I don't think !req.res would be non-nil when got an ECONNRESET error,
That's an implementation detail that might change in the future. There are PR's that might or might not be merged that changes this.
@ronag the http spec said something about the "data race" but didn't give a solution.
The |
That's unfortunate. Chromium seems to assume that a 408 reply is sent before the socket is closed, is that a possible solution? i.e. if the server is about to close the socket due to timeout, first push out a 408 timed out on the wire which the client would detect in the case of this race condition? |
@ronag I think 408 and |
Interesting, they seem to always retry regardless of idempotency? |
@mcollina: I would consider if it might be appropriate to handle this race condition edge case in the Node core client code? I don't think most users consider this and I would hope there is at least something we could do to help users here, e.g. similar to @themez example retry automatically in certain situations? |
@themez This PR is about to land. User name and email address in these commits seems not be linked to your Github account. Would you like to change the name/email in commits, or add the email to your Github email list ? You can take a look at: https://github.com/nodejs/node/blob/99b1ada/doc/guides/contributing/pull-requests.md#step-1-fork |
Set ClientRequest.reusedSocket property when reusing socket for request, so user can handle retry base on wether the request is reusing a socket. Refs: request/request#3131
@starkwang Looks I was using an old invalid git config by mistake, however, I've changed the email address in commits now, except the first commit looks odd, do you think it is normal? |
@themez That's fine. |
Set ClientRequest.reusedSocket property when reusing socket for request, so user can handle retry base on wether the request is reusing a socket. Refs: request/request#3131 PR-URL: #29715 Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Weijia Wang <[email protected]>
Landed in 8915b15 |
Set ClientRequest.reusedSocket property when reusing socket for request, so user can handle retry base on wether the request is reusing a socket. Refs: request/request#3131 PR-URL: #29715 Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Weijia Wang <[email protected]>
Set ClientRequest.reusedSocket property when reusing socket for request, so user can handle retry base on wether the request is reusing a socket. Refs: request/request#3131 PR-URL: #29715 Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Weijia Wang <[email protected]>
Currently It's hard to handle keep-alive connection closes at unfortunate time. This PR set ClientRequest.reusedSocket property when reusing socket for request, so user can handle retry base on wether the request is reusing a socket. Similar to what chromium did
Refs:
Checklist
make -j4 test
(UNIX), orvcbuild test
(Windows) passes