Skip to content

Conversation

@mshustov
Copy link
Contributor

@mshustov mshustov commented Jan 16, 2020

Summary

Closes #44302
Provides an alternative to hapi request.disconnect event, which uses aborted under the hood.
The usage example in the Legacy platform


The aborted$ event provided via Observable interface as proposed in #44302 (comment)

  • to provide a unified interface for all updateable data sources
  • to manage subscription mechanism under the hood

Usage example:

router.get({ path: '/', validate: false }, async (context, request, res) => {
  const controller = new AbortController();
  request.events.aborted$.subscribe(controller.abort);

  const client = context.core.elasticsearch.dataClient
  return res.ok({
    body: await client.callAsCurrentUser('endpoint', {}, {signal: controller.signal});
  });
});

Checklist

Use strikethroughs to remove checklist items you don't feel are applicable to this PR.

For maintainers

Dev Docs

KibanaRequest provides aborted$ observable that emits once if and when an incoming request has been aborted.

Usage example:

router.get({ path: '/', validate: false }, async (context, request, res) => {
  const controller = new AbortController();
  request.events.aborted$.subscribe(controller.abort);

  const client = context.core.elasticsearch.dataClient
  return res.ok({
    body: await client.callAsCurrentUser('endpoint', {}, {signal: controller.signal});
  });
});

}

private getEvents(request: Request): KibanaRequestEvents {
const end$ = fromEvent<void>(request.raw.req, 'end').pipe(first());
Copy link
Contributor Author

@mshustov mshustov Jan 17, 2020

Choose a reason for hiding this comment

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

I added end$ to complete aborted$ observable to prevent memory leaks. I use the end event here to make the tests green https://github.com/elastic/kibana/pull/55061/files#diff-7ffe36ae4fa33e0fddf01a3182306eedR47
From my understanding end cannot be used here, as it's emitted when a request payload is fully consumed. It seems that I should use close instead, but switching to close fails the tests (end$ is not completed). I have a question: What is the correct way to understand that request won't emit anymore to complete aborted$?

Note: I don't set connection: keep-alive/close explicitly in tests, I assume the code should work for either case.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@elastic/kibana-platform @watson any thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not that familiar with rxjs, but I'll do my best to answer.

You stepped right into the most complicated part of Node.js 😅 Streams can "end" in many ways unfortunately, it differs with the type of stream, and in some cases also with the version of Node.js, and it's often hard to implement correctly without memory leaks - as you also found out.

A normal request/response event-lifecycle looks something like this:

image

In case the underlying socket is destroyed the order of events are like so:

image

Note: If I want to be sure to be notified when a stream ends I always use the end-of-stream module.

Pipe only cares about the end event, so if you pipe source into target, but source then emits a close event, target will not know about it and the pipe will still be open:

image

Similar to the end-of-stream module, there's a pump module which takes care of this edge-case, so that the target stream is correctly closed if the source stream closes. I think that's the issue you're seeing

Note: I don't set connection: keep-alive/close explicitly in tests, I assume the code should work for either case.

Correct, the events on the request and response objects shouldn't be affected by the Connection header.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great explanation, really appreciate it!
I surprised to see Server: Socket: closed in both your examples. From my understanding server should keep the socket open since HTTP/1.1 uses connection: keep-alive by default. What I missed here?

https://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html

A significant difference between HTTP/1.1 and earlier versions of HTTP is that persistent connections are the default behavior of any HTTP connection. That is, unless otherwise indicated, the client SHOULD assume that the server will maintain a persistent connection, even after error responses from the server.

What I missed here?

Copy link
Contributor

Choose a reason for hiding this comment

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

In my example, I used the default global HTTP Agent in Node.js which doesn't keep connections alive. If I set it to keep-alive, it looks like this:

image

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@watson is this built-in nodejs logger?

Copy link
Contributor

Choose a reason for hiding this comment

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

Nope, I'm using my own event debugging module: https://www.npmjs.com/package/event-debug

Copy link
Contributor Author

@mshustov mshustov Jan 20, 2020

Choose a reason for hiding this comment

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

Then end usage in the code looks correct:

  • server handles an incoming request and responds. end emitted --> aborted$ completes --> end$ completes
  • an incoming request aborted / socket destroyed. aborted emitted --> aborted$ completes --> end$ unsubscribes from end event (there are no other subscriptions).

Therefore memory a memory leak should not be possible.
Additionally, I performed a loading test locally and everything looks good. load test
Using additional package https://github.com/mafintosh/end-of-stream looks a bit overkill for such a small use-case.
@elastic/kibana-platform are you okay with the current implementation or we should consider exposing the EventEmitter interface instead of Observables? Then we needn't to think about cleaning up after manually created Observable (GC removes all listeners)

@mshustov mshustov added Feature:New Platform release_note:plugin_api_changes Contains a Plugin API changes section for the breaking plugin API changes section. Team:Core Platform Core services: plugins, logging, config, saved objects, http, ES client, i18n, etc t// labels Jan 17, 2020
@elasticmachine
Copy link
Contributor

Pinging @elastic/kibana-platform (Team:Platform)

@mshustov mshustov changed the title [NP] KibanaRequest provides request abortion event [PoC] [NP] KibanaRequest provides request abortion event Jan 17, 2020
@mshustov mshustov marked this pull request as ready for review January 20, 2020 09:07
@mshustov mshustov requested a review from a team as a code owner January 20, 2020 09:07
Copy link
Contributor

@pgayvallet pgayvallet left a comment

Choose a reason for hiding this comment

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

implementation is looking good to me and follow consensus of #44302

Comment on lines 164 to 166
return {
aborted$: fromEvent<void>(request.raw.req, 'aborted').pipe(first(), takeUntil(end$)),
} as const;
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm wondering if we should add areplay effect. Is there a risk for the request to already be aborded when entering the request handler?

Copy link
Contributor Author

@mshustov mshustov Jan 20, 2020

Choose a reason for hiding this comment

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

That's a tricky part... the underlying framework should handle it. How to test it? No clue. We can add sharedReplay to be double-safe.

@mshustov mshustov changed the title [PoC] [NP] KibanaRequest provides request abortion event [NP] KibanaRequest provides request abortion event Jan 20, 2020
@mshustov mshustov requested review from a team and pgayvallet January 20, 2020 16:59
@kibanamachine
Copy link
Contributor

💚 Build Succeeded

History

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

@mshustov mshustov merged commit 3e69ea5 into elastic:master Jan 22, 2020
@mshustov mshustov deleted the issue-44302-abort-events branch January 22, 2020 12:35
mshustov added a commit to mshustov/kibana that referenced this pull request Jan 22, 2020
* add aborted$ observable to KibanaRequest

* complete observable on request end

* update docs

* update test suit names

* always finish subscription

* address comments
jloleysens added a commit to jloleysens/kibana that referenced this pull request Jan 22, 2020
…t-of-legacy

* 'master' of github.com:elastic/kibana:
  Restore 200 OK + response payload for save and delete endpoints (elastic#55535)
  [SIEM] [Detection Engine] Log time gaps as failures for now (elastic#55515)
  [NP] KibanaRequest provides request abortion event (elastic#55061)
mshustov added a commit that referenced this pull request Jan 22, 2020
* add aborted$ observable to KibanaRequest

* complete observable on request end

* update docs

* update test suit names

* always finish subscription

* address comments
@lukasolson
Copy link
Contributor

lukasolson commented Feb 27, 2020

Any possibility to backport to 7.6.1? (See #58749)

mshustov added a commit to mshustov/kibana that referenced this pull request Feb 28, 2020
* add aborted$ observable to KibanaRequest

* complete observable on request end

* update docs

* update test suit names

* always finish subscription

* address comments
@mshustov
Copy link
Contributor Author

mshustov commented Feb 28, 2020

@lukasolson #58851 backport to 7.6 branch. which version label I should use? 7.6.1/7.6.2?

mshustov added a commit that referenced this pull request Feb 28, 2020
* add aborted$ observable to KibanaRequest

* complete observable on request end

* update docs

* update test suit names

* always finish subscription

* address comments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backported Feature:New Platform release_note:plugin_api_changes Contains a Plugin API changes section for the breaking plugin API changes section. Team:Core Platform Core services: plugins, logging, config, saved objects, http, ES client, i18n, etc t// v7.6.1 v7.7.0 v8.0.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Kibana should allow ES request cancellation when browser disconnects

7 participants