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

Stop using aborted event for KibanaRequest.events.aborted$ #126184

Merged
merged 7 commits into from
Feb 23, 2022

Conversation

pgayvallet
Copy link
Contributor

@pgayvallet pgayvallet commented Feb 22, 2022

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 our POST or PUT endpoints).

This was caused by a regression deliberate (and undocumented) change in the way node's IncomingMessage 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 node v16.12.0 (https://nodejs.org/docs/latest-v16.x/api/http.html#event-abort)

Checklist

@pgayvallet pgayvallet added bug Fixes for quality problems that affect the customer experience v8.0.1 v8.1.0 v8.2.0 labels Feb 22, 2022
Comment on lines +349 to +351
function isCompleted(request: Request) {
return request.raw.res.writableFinished;
}
Copy link
Contributor Author

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)

Comment on lines 220 to 234
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$)
);
Copy link
Contributor Author

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.

Copy link
Member

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?

Suggested change
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?

Copy link
Contributor

@gsoldevila gsoldevila Feb 23, 2022

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());

Copy link
Contributor

@gsoldevila gsoldevila Feb 23, 2022

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).

Copy link
Contributor Author

@pgayvallet pgayvallet Feb 23, 2022

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.

Copy link
Contributor

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().

Suggested change
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);
})
);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nevermind regarding adding replay to aborted$, it was probably very late yesterday. changes in 19b120b and df3173b

Copy link
Contributor Author

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.

@pgayvallet pgayvallet added release_note:skip Skip the PR/issue when compiling release notes Team:Core Core services & architecture: plugins, logging, config, saved objects, http, ES client, i18n, etc Feature:http labels Feb 22, 2022
@pgayvallet pgayvallet marked this pull request as ready for review February 22, 2022 21:24
@pgayvallet pgayvallet requested a review from a team as a code owner February 22, 2022 21:24
@elasticmachine
Copy link
Contributor

Pinging @elastic/kibana-core (Team:Core)

Copy link
Member

@afharo afharo left a 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

@kibana-ci
Copy link
Collaborator

💚 Build Succeeded

Metrics [docs]

✅ unchanged

History

To update your PR or re-run it, just comment with:
@elasticmachine merge upstream

@pgayvallet pgayvallet added the auto-backport Deprecated: Automatically backport this PR after it's merged label Feb 23, 2022
@pgayvallet pgayvallet merged commit d053a7f into elastic:main Feb 23, 2022
kibanamachine pushed a commit to kibanamachine/kibana that referenced this pull request Feb 23, 2022
…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)
kibanamachine pushed a commit to kibanamachine/kibana that referenced this pull request Feb 23, 2022
…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)
@kibanamachine
Copy link
Contributor

💚 All backports created successfully

Status Branch Result
8.1
8.0

Note: Successful backport PRs will be merged automatically after passing CI.

Questions ?

Please refer to the Backport tool documentation

kibanamachine added a commit that referenced this pull request Feb 23, 2022
…) (#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>
kibanamachine added a commit that referenced this pull request Feb 23, 2022
…) (#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>
lucasfcosta pushed a commit to lucasfcosta/kibana that referenced this pull request Mar 2, 2022
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
auto-backport Deprecated: Automatically backport this PR after it's merged bug Fixes for quality problems that affect the customer experience Feature:http release_note:skip Skip the PR/issue when compiling release notes Team:Core Core services & architecture: plugins, logging, config, saved objects, http, ES client, i18n, etc v8.0.1 v8.1.0 v8.2.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Core] aborted$ stream defined in core/server/http/router/request.ts doesn't work in some cases
6 participants