-
Notifications
You must be signed in to change notification settings - Fork 8.3k
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
Stop using aborted
event for KibanaRequest.events.aborted$
#126184
Stop using aborted
event for KibanaRequest.events.aborted$
#126184
Conversation
function isCompleted(request: Request) { | ||
return request.raw.res.writableFinished; | ||
} |
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 the workaround recommended way to distinguish if a response was properly completed or aborted when a close
event is fired, given nodejs/node#40775 (comment)
const finished$ = fromEvent<void>(request.raw.res, 'close').pipe( | ||
filter(() => { | ||
return isCompleted(request); | ||
}), | ||
first() | ||
); | ||
|
||
// the response's underlying connection was terminated prematurely | ||
const aborted$ = fromEvent<void>(request.raw.res, 'close').pipe( | ||
filter(() => { | ||
return !isCompleted(request); | ||
}), | ||
first(), | ||
takeUntil(finished$) | ||
); |
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 initially tried to use only one source fromEvent
observable, two destination subjects, and manually emitting to these depending on isCompleted
, but the implementation was, in the end, more complex, and more vulnerable to edge cases such as errors. Given we were already using fromEvent
twice before these changes, I think it's acceptable anyway.
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.
RxJS noob question here: do we still get any benefit from having one fromEvent
observable?
const finished$ = fromEvent<void>(request.raw.res, 'close').pipe( | |
filter(() => { | |
return isCompleted(request); | |
}), | |
first() | |
); | |
// the response's underlying connection was terminated prematurely | |
const aborted$ = fromEvent<void>(request.raw.res, 'close').pipe( | |
filter(() => { | |
return !isCompleted(request); | |
}), | |
first(), | |
takeUntil(finished$) | |
); | |
const closed$ = fromEvent<void>(request.raw.res, 'close'); | |
const finished$ = closed$.pipe( | |
filter(() => { | |
return isCompleted(request); | |
}), | |
first() | |
); | |
// the response's underlying connection was terminated prematurely | |
const aborted$ = closed$.pipe( | |
filter(() => { | |
return !isCompleted(request); | |
}), | |
first(), | |
takeUntil(finished$) | |
); |
I'm not sure about the internal implementation of RxJS, but it looks like it'd create only one listener to the close
event? Question is... would it work as expected?
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.
From the fromEvent documentation:
Every time resulting Observable is subscribed, event handler function will be registered to event target on given event type. When that event fires, value passed as a first argument to registered function will be emitted by output Observable. When Observable is unsubscribed, function will be unregistered from event target.
To answer @afharo 's question, I created a small StackBlitz to confirm that 2 listeners are created / attached anyway with the proposed approach.
If you really want to have a single listener to the original event, there is no need to create Subjects and emit to them. I believe you can simply use the share operator:
const closed$ = fromEvent<void>(request.raw.res, 'close').pipe(share());
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 wonder whether the finished$
and aborted$
Observables should also have the replay behavior.
Your proposed finished$
does not have it anymore (as opposed to finish$
), which means if somebody subscribes to the event after it has happened they will just miss it and might get stuck.
IMHO there's no harm in replaying the last event (if it exists) for this type of network related events (we do have a first()
, which ensures we'll only be getting one).
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.
Your proposed finished$ does not have it anymore (as opposed to finish$),
That's true, but finished$
is only internal and the public completed$
observable subscribes to it 'instantly' (no return to the ev loop, so no risk for the event to fire in the middle), and has a replay effect, so I think it was unnecessary to have it in two places.
Regarding aborted$
, there was no replay effect before the PR, and adding one did break tests, so I felt it was safer to KISS and preserve the original behavior as much as possible.
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.
Also, perhaps we can reorder the operators and use first()
first, this way there is no need to use takeUntil()
.
const finished$ = fromEvent<void>(request.raw.res, 'close').pipe( | |
filter(() => { | |
return isCompleted(request); | |
}), | |
first() | |
); | |
// the response's underlying connection was terminated prematurely | |
const aborted$ = fromEvent<void>(request.raw.res, 'close').pipe( | |
filter(() => { | |
return !isCompleted(request); | |
}), | |
first(), | |
takeUntil(finished$) | |
); | |
const closed$ = fromEvent<void>(request.raw.res, 'close').pipe( | |
shareReplay(1), | |
first(), | |
); | |
const finished$ = $closed.pipe( | |
filter(() => { | |
return isCompleted(request); | |
}) | |
); | |
// the response's underlying connection was terminated prematurely | |
const aborted$ = $closed.pipe( | |
filter(() => { | |
return !isCompleted(request); | |
}) | |
); |
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.
Also, perhaps we can reorder the operators
Definitely better, yea. PR updated.
Pinging @elastic/kibana-core (Team:Core) |
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! Just have a nit question (also RxJS noob question): https://github.com/elastic/kibana/pull/126184/files#r812712848
💚 Build SucceededMetrics [docs]
History
To update your PR or re-run it, just comment with: |
…ic#126184) * Stop using `aborted` event for `KibanaRequest.events.aborted$` * add another test, just in case * use a single `fromEvent` * add replay effect to aborted$ * improve impl * remove useless bottom-stream replay * yup, that's simpler (cherry picked from commit d053a7f)
…ic#126184) * Stop using `aborted` event for `KibanaRequest.events.aborted$` * add another test, just in case * use a single `fromEvent` * add replay effect to aborted$ * improve impl * remove useless bottom-stream replay * yup, that's simpler (cherry picked from commit d053a7f)
💚 All backports created successfully
Note: Successful backport PRs will be merged automatically after passing CI. Questions ?Please refer to the Backport tool documentation |
…) (#126249) * Stop using `aborted` event for `KibanaRequest.events.aborted$` * add another test, just in case * use a single `fromEvent` * add replay effect to aborted$ * improve impl * remove useless bottom-stream replay * yup, that's simpler (cherry picked from commit d053a7f) Co-authored-by: Pierre Gayvallet <pierre.gayvallet@elastic.co>
…) (#126248) * Stop using `aborted` event for `KibanaRequest.events.aborted$` * add another test, just in case * use a single `fromEvent` * add replay effect to aborted$ * improve impl * remove useless bottom-stream replay * yup, that's simpler (cherry picked from commit d053a7f) Co-authored-by: Pierre Gayvallet <pierre.gayvallet@elastic.co>
…ic#126184) * Stop using `aborted` event for `KibanaRequest.events.aborted$` * add another test, just in case * use a single `fromEvent` * add replay effect to aborted$ * improve impl * remove useless bottom-stream replay * yup, that's simpler
Summary
Fix #125240
Fix a problem causing
KibanaRequest.events.aborted$
to not emit in scenarios where it should, when used within endpoints consuming the payload (basically most of ourPOST
orPUT
endpoints).This was caused by a
regressiondeliberate (and undocumented) change in the way node'sIncomingMessage
internally works. See nodejs/node#40775 for more context.Also has the upside to stop using that
aborted
event which is flagged as deprecated since nodev16.12.0
(https://nodejs.org/docs/latest-v16.x/api/http.html#event-abort)Checklist