-
-
Notifications
You must be signed in to change notification settings - Fork 956
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
Use events to wire upload tracking instead of racing with setImmediate #525
Conversation
@@ -122,8 +120,9 @@ module.exports = (options = {}) => { | |||
|
|||
cacheReq.once('request', req => { | |||
let aborted = false; | |||
req.once('abort', _ => { | |||
req.once('abort', () => { |
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.
Refactoring should be done in a separate PR. Just Kidding :P 👍
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 considered it, but when I looked at the diff, I didn't think the one line change raised the cognitive burden enough to warrant a separate commit. 😅
source/request-as-event-emitter.js
Outdated
aborted = true; | ||
emitter.emit('abort'); |
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.
Do we really need that? It's aborted only when receives an error/someone cancels the request. The error got
throws has nice details though.
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's not needed. I don't know why I put it there. 🤪
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 know.
2064ea8
to
16125cc
Compare
} catch (error) { | ||
emitter.emit('error', error); | ||
} | ||
}); |
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 suspect this was added to control the call stack, ensuring that the method ran to completion before any errors were emitted. This changes that behavior. There doesn't appear to be any tests failing from the change, but I would guess that this introduces behavioral changes.
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.
Originally added in #117.
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.
@brandon93s Thanks for the reference to #117.
It looks like the intent was to ensure the response data was handled after the event listeners that clear timeouts were run.
If a callback is provided for request
, it will be the first handler to run.
I can make another pass at this, to evaluate when the timeout clearing handlers are being run.
Does my analysis sound right? See any other issues?
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 both are right. The main intent was to emit response ASAP, so @floatdrop used setImmediate
as it executes immediately after I/O operations. I'd leave setImmediate
unchanged here.
If a callback is provided for request, it will be the first handler to run.
I think @jstewmon is right here. We don't need setImmediate
in this case (line 198).
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 main intent was to emit response ASAP
That's not how I read it. There are 3 emitters involved, so I'm not sure which one you mean. The emitter
response event is emitted from getResponse
, so setImmediate
here delays that. The req
emitter's event isn't affected by this setImmediate
.
This is the background information I found:
The version of timed-out
being used at the time of #117 listened for the response
event of the request to clear the timeout:
https://github.com/floatdrop/timed-out/blob/v2.0.0/index.js#L31
The request
object was passed to timed-out, which would setup that listener.
The statement that was deferred with setImmediate
called unzipResponse
from the http.request
callback, which would have run before timed-out's listener.
So my assessment was that the intent was to allow timed-out's listener to run before the response stream was connected to unzipResponse.
Now that timeout.request
is waiting for response.end
, I think deferring the call to getResponse
actually increases the possibility that the timeout will breach.
Since it is quite common to use a request callback, I think a better solution to checking a timeout against the response
event would be to use setImmediate in the timer callback to defer the error emission to the check
phase of the event loop in case the response had been received but was still queued due to load. At a minimum, the listener that clears the timeout could be subscribed with prependOnceListener
to ensure it runs earlier.
I thought there might be some practical purpose other than the ordering of the response handlers (like allowing subscriptions to some event emitter), but I don't see any such purpose, and I think we can clearly see that the response
event listener order is no longer applicable.
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 a better solution to checking a timeout against the response event would be to use setImmediate in the timer callback to defer the error emission to the check phase of the event loop in case the response had been received but was still queued due to load.
So the connection was established, because there's a response.
Here's some code to illustrate what I meant about using setImmediate with timers.
You were trying to illustrate the first sentence using the connect
timeout, though it had been connected already. Awesome.
Is it possible to receive connect
event and response
(with some queued data) in the same loop?
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.
Have you carefully reviewed the doc I linked earlier about the event loop phases?
Yes, I have read it several times, not only today.
The point is that if we're checking connect timeout. The connection may have already been established, but its event hasn't fired when the timeout callback is run.
Then the connection is not established. From the docs:
Event: 'connect'
Emitted when a socket connection is successfully established.
socket.connecting
If true - socket.connect(options[, connectListener]) was called and haven't yet finished. Will be set to false before emitting 'connect' event and/or calling socket.connect(options[, connectListener])'s callback.
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 is a tricky topic because of the changes in behavior over time.
When I've referred to the response
event, I've been talking about the behavior and intent at the time (circa 2015) that the setImmediate deferral was added in #117. The response
event is not currently used to clear any timeouts, so it is not relevant to this change.
You were trying to illustrate the first sentence using the connect timeout, though it had been connected already.
No, the statement you quoted was in my original analysis. I was suggesting that there was a more precise way of mitigating #117 than deferring the response piping with setImmediate.
I didn't want to use the response event to illustrate how that suggestion would be implemented because none of the current timeouts depend on the timing of that event. I used the connect
event as an example in the current code because it is used to clear the connect timeout.
Is it possible to receive connect event and response (with some queued data) in the same loop?
I don't see any reason why that would not be possible, but their listeners would be notified in FIFO order (connect before response). Does it matter?
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.
Then the connection is not established
The underlying system operation completes before we're notified. That's the purpose of the poll phase - to notify us that the IO operations we listened for are complete.
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.
Now that timeout.request is waiting for response.end, I think deferring the call to getResponse actually increases the possibility that the timeout will breach.
Not true. Before I've copied timed-out
into this repo there was pTimeout
handling timeout.request
, so it started counting immediately after the promise was called (no connection has been made yet). That's just only a note.
The underlying system operation completes before we're notified. That's the purpose of the poll phase - to notify us that the IO operations we listened for are complete.
I still disagree here. It has its own time to establish the connection. But this change is not significant I think. Let's skip this subject.
No, the statement you quoted was in my original analysis. I was suggesting that there was a more precise way of mitigating #117 than deferring the response piping with setImmediate.
So if these are two different things, I've got only one question: what is the better way to avoid bottleneck with emitting the response and handling it? (Line 107) What else if not setImmediate? You haven't implemented it yet. It just delays establishing another connections for now (if there's big amount of requests).
source/request-as-event-emitter.js
Outdated
@@ -148,8 +146,7 @@ module.exports = (options = {}) => { | |||
total: uploadBodySize | |||
}); | |||
|
|||
const socket = req.connection; | |||
if (socket) { | |||
req.once('socket', 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.
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've reviewed those issues and do not think this change regresses either of them.
- The problem reported in Uncaught TypeError: Cannot read property 'once' of undefined at EventEmitter.ee.once.req #427 and fixed in Check req.connection exists before attaching event listeners #429 was that
req.connection
may be undefined. I've removed the reference toreq.connection
in favor of subscribing to thesocket
event to get theconnection
. So, this should not regress that issue b/c we don't try to access a property which may be undefined. - The memory leak fix from Fix socket 'connect' memory leak #465 is addressed by the
if (socket.connecting)
check. It prevents unbounded event subscriptions with keep-alive is enabled on the agent.
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've looked deeply and I think it's safe to do so. Good job!
source/request-as-event-emitter.js
Outdated
}); | ||
|
||
if (options.gotTimeout) { | ||
clearInterval(progressInterval); | ||
timedOut(req, options.gotTimeout); | ||
} | ||
|
||
setImmediate(() => { |
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.
Note: this setImmediate deferral was introduced in a1eb3f7 in exchange for get
being deferred. get
is currently deferred (didn't track down the commit), so this does not need to be deferred.
Either get
or emitter.emit('request', req)
needs to be deferred to give callers a chance to subscribe to the request
event.
16125cc
to
9d4c0e6
Compare
9d4c0e6
to
cd76c42
Compare
Closing in favor of #534 |
I could not determine why
setImmediate
was being used to defer some of the statements in the event emitter wiring, other than the socket connection check, which can be safely done by subscribing to the right events.