Skip to content

Update Delete Remote Cluster API to support multiple comma-delimited clusters.#34595

Merged
cjcenizal merged 7 commits intoelastic:masterfrom
cjcenizal:bug/remove-remote-clusters-promises
Apr 8, 2019
Merged

Update Delete Remote Cluster API to support multiple comma-delimited clusters.#34595
cjcenizal merged 7 commits intoelastic:masterfrom
cjcenizal:bug/remove-remote-clusters-promises

Conversation

@cjcenizal
Copy link
Copy Markdown
Contributor

@cjcenizal cjcenizal commented Apr 4, 2019

This fixes a bug in how success and failure status is reported in the UI when you delete multiple remote clusters. Originally there was a race condition which prevented accurate reporting of this status.

image

image

@cjcenizal cjcenizal added release_note:fix Feature:CCR and Remote Clusters v8.0.0 Team:Kibana Management Dev Tools, Index Management, Upgrade Assistant, ILM, Ingest Node Pipelines, and more t// v7.2.0 v7.0.1 labels Apr 4, 2019
@cjcenizal cjcenizal requested review from jen-huang and sebelga April 4, 2019 23:36
@elasticmachine
Copy link
Copy Markdown
Contributor

Pinging @elastic/es-ui

@elasticmachine
Copy link
Copy Markdown
Contributor

💔 Build Failed

const removalSuccesses = [];
const removalErrors = [];
const removeClusterRequests = names.map(name => {
sendRemoveClusterRequest(name)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is an error because no promise is returned, so removeClusterRequests didn't represent the actual requests being sent. This resulted in a race condition on line 35, because we only wait as long as the timing promise on line 38 takes to resolve instead of waiting for our API requests to resolve.

As a result, the toasts we display won't be accurate reflections of what was deleted if the requests take longer to resolve than 500ms. I never caught this bug because I never tested the UI with a high-latency connection (simulated or otherwise).

While I was fixing this, I also noticed how inefficient this implementation is because it sends out multiple requests. I decided to adopt the approach we've taken elsewhere by updating the API endpoint to support multiple remote clusters in a single DELETE request.

@elasticmachine
Copy link
Copy Markdown
Contributor

💚 Build Succeeded

Copy link
Copy Markdown
Contributor

@sebelga sebelga left a comment

Choose a reason for hiding this comment

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

Thanks for spotting this bug and fixing it @cjcenizal ! It works very well but it was a bit hard to read the server handler function. I made a suggestion to what I think makes it easier to follow.
I also think the client action could be simplified a bit by using the same variable names that the request response. Let me know what you think 😊

.then(() => removalSuccesses.push(name))
.catch(() => removalErrors.push(name));
});
let removalSuccesses;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why not use the same variable names as the request response? ("itemsDeleted" and "errors").
Also, I removed the 500ms timeout and I can't not see the flicker in the comment.

I also wonder if we shouldn't have a catch in case the server throws an error...

dispatch({
  type: REMOVE_CLUSTERS_START,
});

const clustersToDelete = names.join(',');
const { itemsDeleted, errors } = await sendRemoveClusterRequest(clustersToDelete)
  .then(({ data }) => data)
  .catch(() => ({ errors: clustersToDelete, itemsDeleted: [] }));

if (errors.length > 0) {
   ...

const itemsDeleted = [];
const errors = [];

await Promise.all(names.map((clusterName) => (
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think it would be clearer to extract the function and give it a name (like I did in rollup https://github.com/elastic/kibana/blob/master/x-pack/plugins/rollup/server/routes/api/jobs.js#L119).

I also had a hard time to read it, I would suggest to split it in parts. What do you think about this:

handler: async (request) => {
  const callWithRequest = callWithRequestFactory(server, request);
  const { name } = request.params;
  const names = name.split(',');

  const itemsDeleted = [];
  const errors = [];

  // Validator that returns an error if the remote cluster does not exist
  const validateClusterDoesExist = async (name) => {
    try {
      const existingCluster = await doesClusterExist(callWithRequest, name);
      return existingCluster
        ? null
        : wrapCustomError(new Error(`The cluster ${name} does not exist.`), 404);
    } catch (err) {
      return wrapCustomError(err, 400);
    }
  };

  // Send the request to delete the cluster and returns an error if it could not be deleted
  const sendRequestToDeleteCluster = async (name) => {
    const body = serializeCluster({ name });

    try {
      const response = await callWithRequest('cluster.putSettings', { body });
      const acknowledged = get(response, 'acknowledged');
      const cluster = get(response, `persistent.cluster.remote.${name}`);

      if (acknowledged && !cluster) {
        return null;
      }

      // If for some reason the ES response still returns the cluster information,
      // return an error. This shouldn't happen.
      return wrapCustomError(new Error('Unable to delete cluster, information still returned from ES.'), 400);
    } catch(err) {
      return isEsError(err)
        ? wrapEsError(err)
        : wrapUnknownError(err);
    }
  };

  const deleteCluster = async (clusterName) => {
    try {
      // Validate that the cluster exists
      let error = await validateClusterDoesExist(clusterName);

      if (!error) {
        // Delete the cluster
        error = await sendRequestToDeleteCluster(clusterName);
      }

      if (error) {
        throw error;
      }

      // If we are here, it means that everything went well...
      itemsDeleted.push(clusterName);
    } catch(err) {
      errors.push({ name: clusterName, error: err });
    }
  };

  // Delete all our cluster in parallel
  await Promise.all(names.map(deleteCluster));

  return {
    itemsDeleted,
    errors,
  };
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I like this much better! Thanks for the great suggestion.

@cjcenizal
Copy link
Copy Markdown
Contributor Author

cjcenizal commented Apr 5, 2019

Thanks for the review @sebelga! I've implemented your suggestions. Can you please take another look?

Regarding the flicker, I'm able to reproduce it regularly. If the request resolves more quickly than 500ms then I see it.

@elasticmachine
Copy link
Copy Markdown
Contributor

💔 Build Failed

@elasticmachine
Copy link
Copy Markdown
Contributor

💔 Build Failed

@elasticmachine
Copy link
Copy Markdown
Contributor

💚 Build Succeeded

Copy link
Copy Markdown
Contributor

@sebelga sebelga left a comment

Choose a reason for hiding this comment

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

LGTM! Thanks for updating the handler. I'm not convinced by the new variable name but hey... it's just a variable name (one of the two hard things in computer science!) 😄

}),
// Wait at least half a second to avoid a weird flicker of the saving feedback.
// Wait at least half a second to avoid a weird flicker of the saving feedback (only visible
// when requests resolve very quickly).
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It would be interesting to know why this app requires this hack and others don't. Let's leave it for now and you show it to me over zoom at some point. 😊

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ideally we should do this anywhere we display a loading state and we care about a flicker disrupting the UX, though AFAIK it's inconsistently implemented. Here's how it looks without a minimum delay:

async_feedback_flicker

Copy link
Copy Markdown
Contributor

@sebelga sebelga Apr 9, 2019

Choose a reason for hiding this comment

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

I see... it's kind of the React scheduler that we will get. I might have solved it differently to avoid adding a hack on each http request.
Wrap the full-screen eui loader with an HOC that sets a timeout of 300ms, and only after that time it shows the component. If it's less than 300 ms, don't do anything.

This way, all of Kibana benefit from it and we don't have to add Promise.all.

} = errors[0];

const title = getErrorTitle(errors.length, name);
toastNotifications.addDanger({
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nice improvement!

const callWithRequest = callWithRequestFactory(server, request);
const { name } = request.params;
const names = name.split(',');
const { nameOrNames } = request.params;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This seems over necessary to me. If you prefer to call it names (plural) to make it clearer that it can accept more than 1 item, we could (although then we'd have a problem of naming with the local var).

This pattern is very common in Javascript to have to explicitly write it in the variable name

function something(id: string | string[]) {
  const ids = arrify(id);
   ...
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I like the pattern you're proposing, but in your example the input is an array or string that gets coerced to an array. In this case, the input is always a string, which we're splitting into an array. I'm not sure I see how the pattern can be applied here -- what am I missing?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sorry, I was missing the fact that indeed this is different from the example I was giving.
I guess that here, if the name of the param was names, and I read this code

const { names } = request.params;
const namesArray = names.split(',');

// At this point it is clear that we have an array of names (one or many is not important)
....

If this was a public API, the doc would clearly specify that the name can be a list of names separated by a comma. And here as a private API I am not sure that being that explicit brings a lot. But as I said in my first comment, I don't feel strongly about this 😊

@cjcenizal
Copy link
Copy Markdown
Contributor Author

Thanks for the review @sebelga! I'm going to merge this for the sake of progress but feel free to address #34595 (comment) and I can incorporate your feedback in another PR.

@cjcenizal cjcenizal merged commit b054214 into elastic:master Apr 8, 2019
@cjcenizal cjcenizal deleted the bug/remove-remote-clusters-promises branch April 8, 2019 17:41
cjcenizal added a commit to cjcenizal/kibana that referenced this pull request Apr 8, 2019
…clusters. (elastic#34595)

* Handle a 500 error. Add error message to toast notifications.
cjcenizal added a commit to cjcenizal/kibana that referenced this pull request Apr 8, 2019
cjcenizal added a commit that referenced this pull request Apr 12, 2019
…clusters. (#34595) (#34734)

* Handle a 500 error. Add error message to toast notifications.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Feature:CCR and Remote Clusters release_note:fix Team:Kibana Management Dev Tools, Index Management, Upgrade Assistant, ILM, Ingest Node Pipelines, and more t// v7.0.1 v7.2.0 v8.0.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants