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

cancellation of interpreter execution #40238

Merged
merged 11 commits into from
Jul 30, 2019

Conversation

ppisljar
Copy link
Member

@ppisljar ppisljar commented Jul 3, 2019

Summary

Adds cancelation support to interpreter via AbortSignal

  • AbortSignal is checked on every step of chain, if abort was requested interpreter will return with error and will not continue executing any remaining functions.
  • AbortSignal is also passed to every function as one of the handlers. This way functions that take long time can also cancel early.

Dev Docs

The interpreter now accepts an AbortController signal inside handlers for aborting execution. Since handlers is passed down to each function in the interpreter, functions themselves can handle additional cleanup using handlers.abortSignal if it is available.

Checklist

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

For maintainers

@ppisljar ppisljar added Feature:ExpressionLanguage Interpreter expression language (aka canvas pipeline) Team:AppArch v7.4.0 v8.0.0 labels Jul 3, 2019
@elasticmachine
Copy link
Contributor

Pinging @elastic/kibana-app-arch

@elasticmachine
Copy link
Contributor

💚 Build Succeeded

@@ -128,6 +128,8 @@ export class DashboardAppController {
const panelActionsRegistry = Private(ContextMenuActionsRegistryProvider);
const getUnhashableStates = Private(getUnhashableStatesProvider);
const shareContextMenuExtensions = Private(ShareContextMenuExtensionsRegistryProvider);
const abortController = new AbortController();
Copy link
Member

Choose a reason for hiding this comment

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

I think you're going to want to import abortcontroller-polyfill here as some browsers don't support this natively.

Copy link
Member

Choose a reason for hiding this comment

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

Also I think you're going to want to create a new AbortController for each update, not just one on the controller load.

Copy link
Member

Choose a reason for hiding this comment

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

@lukasolson I had thought about the polyfill question too, because I was worried that any plugin dev who wanted to use this feature would need to deal with the polyfill. But then I noticed we are importing it here: https://github.com/elastic/kibana/blob/master/src/legacy/ui/ui_bundles/app_entry_template.js#L40

I'm not super familiar with how the entry template works, but I assume this means we won't need to re-import that polyfill elsewhere in kibana?

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, you could totally be right. I'll give this PR a spin on IE just to make sure.

@@ -128,6 +128,8 @@ export class DashboardAppController {
const panelActionsRegistry = Private(ContextMenuActionsRegistryProvider);
const getUnhashableStates = Private(getUnhashableStatesProvider);
const shareContextMenuExtensions = Private(ShareContextMenuExtensionsRegistryProvider);
const abortController = new AbortController();
Copy link
Member

Choose a reason for hiding this comment

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

@lukasolson I had thought about the polyfill question too, because I was worried that any plugin dev who wanted to use this feature would need to deal with the polyfill. But then I noticed we are importing it here: https://github.com/elastic/kibana/blob/master/src/legacy/ui/ui_bundles/app_entry_template.js#L40

I'm not super familiar with how the entry template works, but I assume this means we won't need to re-import that polyfill elsewhere in kibana?

@@ -71,6 +71,11 @@ export function interpreterProvider(config: any) {
// if something failed, just return the failure
if (getType(newContext) === 'error') return newContext;

// if execution was aborted return error
if (handlers.abortSignal && handlers.abortSignal.aborted) {
return createError(`abort was requested`);
Copy link
Member

Choose a reason for hiding this comment

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

createError is surfaced in the UI... is this definitely the behavior we want? Like, if we add a button for someone to cancel, do we want to always see an error message, since technically this would be the expected behavior from a user perspective? (Or maybe it should be a confirmation/success message?)

Also, I'm wondering if it would be better to throw an error here instead, and catch it below? That way all of our error handling happens in one place.

Copy link
Member

Choose a reason for hiding this comment

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

Agreed, I've changed it to a simple Error in 0c429c1. We should probably eventually make a separate AbortedError or something of the sort.

@lukasolson
Copy link
Member

I added a few commits here, I don't think we need to have the dashboard itself be responsible for creating the abort signal. I think it makes more sense to have it in the handler itself, seeing as how it determines when a fetch is necessary. It also has a destroy method that the dashboard uses (and visualize) that can also abort. @ppisljar @lukeelmers Let me know what you think of the recent changes. P.S. This is now actually aborting requests in visualize embeddables (both in Visualize and Dashboard).

@elasticmachine
Copy link
Contributor

💔 Build Failed

@@ -71,6 +71,11 @@ export function interpreterProvider(config: any) {
// if something failed, just return the failure
if (getType(newContext) === 'error') return newContext;

// if execution was aborted return error
if (handlers.abortSignal && handlers.abortSignal.aborted) {
return new Error(`abort was requested`);
Copy link
Contributor

@streamich streamich Jul 9, 2019

Choose a reason for hiding this comment

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

Maybe something with word expression in it, like

new Error('Expression was aborted.');

@lukeelmers
Copy link
Member

I think it makes more sense to have it in the handler itself

yeah this makes sense to me too... it feels like a simpler place to start having it centralized in one place, and also being able to rely on the existing destroy method in the embedded visualize handler

Copy link
Member Author

@ppisljar ppisljar left a comment

Choose a reason for hiding this comment

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

regarding moving to loader: then the only way to stop the request is to destroy the loader (embeddable)
which is probably not what we want. If i am looking at the dashboard, and i enter a query, then before i got response i enter another query (very easy ... i enter 20, press enter, i add another 0, to get 200 and press enter) ... now all my embeddables would get destroyed, instead of just updated.

i think we need to make difference between aborting requests and destroying. This doens't mean however that the logic for this can't live in the loader. But on the other hand, lets not forget that we'll want to cancel more than just visualization requests, and long term in most places we won't be using visualize loader anyway.

@@ -102,6 +102,11 @@ const CourierRequestHandlerProvider = function () {
request.stats(getRequestInspectorStats(requestSearchSource));

try {
if (abortSignal) {
abortSignal.addEventListener('abort', () => {
requestSearchSource.cancelQueued();
Copy link
Member Author

Choose a reason for hiding this comment

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

what will this actually do ?

  • cancel queued (so requests that didn't start yet ?)
  • cancel current requests ?
  • on search source or on courier level ? (what happens if the request was batched with another one ?)

Copy link
Member

Choose a reason for hiding this comment

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

It aborts all incomplete requests for this searchSource object. In practice, because of Courier, requests from different searchSource objects are batched up, and cancelling one will cancel all that are bundled within the same _msearch. I don't think this is necessarily a bad thing because it will probably always be what we want. For folks that disable batching, this will only cancel the request for this specific searchSource.

@lukasolson
Copy link
Member

which is probably not what we want. If i am looking at the dashboard, and i enter a query, then before i got response i enter another query (very easy ... i enter 20, press enter, i add another 0, to get 200 and press enter) ... now all my embeddables would get destroyed, instead of just updated.

This isn't quite true... In the PR you can see that we are aborting both when the handler is destroyed as well as when a new fetch is requested.

this.dataLoaderParams.query = params.query;
}
const fetchRequired = ['timeRange', 'filters', 'query'].some(key => params.hasOwnProperty(key));
this.dataLoaderParams = { ...this.dataLoaderParams, ...otherParams };
Copy link
Member

Choose a reason for hiding this comment

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

The reason this was changed is because I had originally added 'abortSignal' in here, and didn't feel like copying/pasting the exact same code 4 times made sense when we could just do it like this. Then I ended up removing 'abortSignal' from here and actually creating it in the handler. I don't mind reverting these changes if it makes people feel more comfortable :)

@lukasolson
Copy link
Member

But on the other hand, lets not forget that we'll want to cancel more than just visualization requests, and long term in most places we won't be using visualize loader anyway.

Totally agreed. I still don't think it makes sense to have the dashboard be responsible for creating the signal and passing it through the dashboard grid/panel/viewport, to the embeddable/handler/loader, etc. The dashboard is responsible for managing state and passing it down to the embeddable. The embeddable/handler is responsible for handling state changes, and determining whether or not a fetch is needed (and in turn the expression is interpreted), and whether or not an existing fetch should be cancelled. Other places that want to cancel the interpreter execution can do things the same way.

@elasticmachine
Copy link
Contributor

💔 Build Failed

@ppisljar
Copy link
Member Author

ppisljar commented Jul 11, 2019

there is still a question on how to handle user aborts.

loader.destroy() cancels queries when we navigate away
loader.update() cancels queries, when we want to refetch

we still need something to handle user clicking on a cancel button (at some point we will provide it).
calling destroy() or update() sounds wrong, we would need something like .cancel() on the loader, which will cancel the existing requests, but won't destroy visualization (so you will still look at the latest render) or we need to pass abort signal to it.

if we decide to expose yet another method, it doesn't need to be part of this PR until we actually need it, but i think we should make the decision on how to handle it now, as if we decide to pass abortsignal in instead of exposing cancel method, that will affect other methods as well (most likely).

any strong opinions about having it either way ?

cc @timroes @joshdover just pinging you here to see whats your opinion on the matter.

@lukasolson
Copy link
Member

Just wanted to also comment loader.reload() will also cancel existing fetches. To me it makes sense to add a cancel() method, similar to how we added the reload method because it was necessary, but I'm open to hearing other possible solutions.

@lukeelmers
Copy link
Member

if we decide to expose yet another method, it doesn't need to be part of this PR until we actually need it, but i think we should make the decision on how to handle it now

Yeah I agree with this; if we don't need it now, let's not expose it. And since we're going to go down the path of exposing a way to manually cancel eventually, then I suppose you are right it makes more sense to put it closer to where we expect it to live in the future, vs in the request handler where would have to be later removed anyway.

To me it makes sense to add a cancel() method, similar to how we added the reload method

++ I totally agree this feels like the natural place to put this if you want to expose a method for users to initiate a cancellation -- so you have reload, destroy, cancel, update, etc.

I suppose the more fundamental design question @ppisljar was getting at is: do we want AbortSignal to be a "thing" app devs must use when building stuff in Kibana, or do we want to abstract that away behind an interface of some sort?

I see pros and cons to each approach; what's probably more important is keeping consistency in our implementations across various services (especially core + app arch services).

@lukasolson
Copy link
Member

I suppose the more fundamental design question @ppisljar was getting at is: do we want AbortSignal to be a "thing" app devs must use when building stuff in Kibana, or do we want to abstract that away behind an interface of some sort?

Yeah, that makes sense. I think having these methods instead of passing in an AbortSignal is desirable. Having an abort signal means you can only listen to when the signal is aborted or read its state (aborted or not). However, if you have the reload, cancel, update, etc. methods, and can create an abort controller/signal in some of these methods, it gives a lot more flexibility. If the loader itself only got an abort signal, it couldn't programmatically abort, but instead would have to rely on something above in the chain aborting. I'm not sure where that would be...

An update, reload, or destroy might not always mean "abort what's already in progress" (although, to be fair, cancel probably does). Having the flexibility to determine if/when they do abort seems beneficial.

For example, say I have an embeddable that actually creates new documents in Elasticsearch. If I submit a request to create a new document, then navigate away before the request completes, I might not actually want the request to be aborted in the destroy method, yet if all I get is an abort signal that I pass on to my Elasticsearch request, that's what will happen.

@lukasolson lukasolson self-assigned this Jul 16, 2019
@lukasolson
Copy link
Member

As I was trimming down this PR to not include the stuff related to actually aborting visualize loading (which will come in a separate PR) I was thinking, abortSignal isn't really something that belongs in the handlers, because handlers is initialized prior to the interpreter being initialized. If it's in handlers, then as soon as you abort(), then any future calls to interpret will return an abort error. If we add it as an additional argument to interpret, I think it makes a lot more sense, because then you can control aborting at a more granular level (individual calls to interpret).

Something like this:

async function invokeChain(chainArr: any, context: any, abortSignal: AbortSignal): Promise<any> {
    if (!chainArr.length) return Promise.resolve(context);

    // if execution was aborted return error
    if (abortSignal && abortSignal.aborted) {
      return new Error('Expression was aborted.');
    }

   ...

I'm going to move forward with this approach unless someone suggests differently.

/cc @lukeelmers @ppisljar @streamich

@elasticmachine
Copy link
Contributor

💔 Build Failed

@lukeelmers
Copy link
Member

(@lukasolson summarizing our offline conversation)

Since the InterpreterProvider is called every time we call the interpret fn (via interpretAst), then any provided handlers (or a separate abortSignal param as you've mentioned) will be given to the individual expression you are wanting to interpret.

So cancellation works either way, whether the signal is in handlers or as its own param.

But the one thing we'll have to solve in either case is how we deal with this in embedded_visualize_handler, which is instantiated once but can trigger multiple calls to the interpreter as params are changed. Presumably this would be via EmbeddedVisualizeHandler.update() and EmbeddedVisualizeHandler.reload()

@elasticmachine
Copy link
Contributor

💚 Build Succeeded

@@ -71,8 +71,16 @@ export function interpreterProvider(config: any) {
// if something failed, just return the failure
if (getType(newContext) === 'error') return newContext;

// if execution was aborted return error
if (handlers.abortSignal && handlers.abortSignal.aborted) {
return createError({
Copy link
Member

Choose a reason for hiding this comment

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

Moved this back to being createError because of the comment below:

The interpreter should never fail. It should always return a {type: error} on failure

The consumers can check if err.name === 'AbortError' and handle those failures differently than other failures.

@lukasolson lukasolson marked this pull request as ready for review July 25, 2019 19:56
@lukasolson lukasolson requested a review from a team as a code owner July 25, 2019 19:56
@lukasolson
Copy link
Member

@ppisljar @lukeelmers I've trimmed this PR down specifically to just be the interpreter portion, and will create a new PR that relies on this one to actually pass in abortSignal inside visualize embeddables, and handles it inside esaggs.

@elasticmachine
Copy link
Contributor

💔 Build Failed

@lukasolson lukasolson added the release_note:plugin_api_changes Contains a Plugin API changes section for the breaking plugin API changes section. label Jul 30, 2019
@elasticmachine
Copy link
Contributor

💔 Build Failed

@lukasolson
Copy link
Member

Retest

@elasticmachine
Copy link
Contributor

💚 Build Succeeded

@lukasolson
Copy link
Member

@ppisljar gave this the LGTM in a message to me, because he couldn't "approve" of his own PR 😄

@lukasolson lukasolson merged commit 305e4a2 into elastic:master Jul 30, 2019
lukasolson pushed a commit to lukasolson/kibana that referenced this pull request Jul 30, 2019
* add AbortSignal to interpreter

* adding AbortSignal to visualize_loader

* adding AbortSignal to embeddables and dashboard

* passing AbortSignal to courier request handler

* Remove abort signal from dashboard and move to handler, and abort fetches when necessary

* Remove the rest of the references to abort signal in dashboard

* Revert changes to dashboard_app

* Remove code related to canceling visualize requests and only keep stuff for canceling interpreter

* Use createError
@lukasolson
Copy link
Member

7.x (7.4.0): 3ccc9a9

jloleysens added a commit to jloleysens/kibana that referenced this pull request Jul 31, 2019
…-or-edit-existing-rollup-job

* 'master' of github.com:elastic/kibana: (114 commits)
  [ML] Fixing empty index pattern list (elastic#42299)
  [Markdown] Shim new platform - cleanup plugin (elastic#41760)
  [Code] Enable hierarchicalDocumentSymbolSupport for java language server (elastic#42233)
  Add New Platform mocks for data plugin (elastic#42261)
  Http server route handler implementation (elastic#41894)
  [SR] Allow custom index pattern to be used instead of selectable list when choosing indices to restore (elastic#41534)
  [Code] distributed Code abstraction (elastic#41374)
  [SIEM] Sets page titles to the current page you are on  (elastic#42157)
  Saved Objects export API stable type order (elastic#42310)
  cancellation of interpreter execution (elastic#40238)
  [SIEM] Fixes a crash when Machine Learning influencers is an undefined value (elastic#42198)
  Changed the job to work with a dedicated index (elastic#42297)
  FTR: fix testSubjects.missingOrFail (elastic#42290)
  Increase retry timeout to prevent flaky tests (elastic#42291)
  Spaces - make space a hidden saved object type (elastic#41688)
  Allow applications to register feature privileges which are excluded from the base privileges (elastic#41300)
  Disable flaky log column reorder test (elastic#42285)
  Fixing add element in element reducer (elastic#42276)
  Fix infinite loop (elastic#42228)
  [Maps][File upload] Remove geojson deep clone logic, handle on maps side (elastic#41835)
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Feature:ExpressionLanguage Interpreter expression language (aka canvas pipeline) release_note:plugin_api_changes Contains a Plugin API changes section for the breaking plugin API changes section. v7.4.0 v8.0.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants