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

[eslint] add rule to forbid async forEach bodies #111637

Merged
merged 15 commits into from
Sep 14, 2021

Conversation

spalger
Copy link
Contributor

@spalger spalger commented Sep 8, 2021

I noticed an unresolved promise rejection on CI and tracked it back to a usage of array.forEach() which was passed an async function. Like #110349 this creates hidden promises which can't be tracked, preventing the rejections from being handled. To prevent this mistake I've implemented an ESLint rule to prevent devs from accidentally using this pattern.

Unlike in the other PR I can't think of a lowest-common-denominator fix to implement across the board, so I have instead fixed all the violations manually. The fixes use one of three new helpers added to @kbn/std:

  1. asyncForEach(iter, fn): The most common replacement was to just replace array.forEach() with a helper that does the same thing. This calls fn synchronously for each item in array and waits for the returned promise/observables to resolve/reject/complete/error.

  2. asyncMap(iter, fn): A common usage of async forEach() was to add items to an array within scope. I have replaced those with asyncMap() which calls fn synchronously for each item in array and waits for the returned promise/observables to resolve/reject/complete/error. The result array has each item in the order of the source array.

  3. map$(iter, fn): A simple wrapper around RxJS for creating a stream from an iterable value where each item in the result is the return value of calling the map function with each item from the iterable. This is the basis of the other functions.

  4. No need for async: There were a handful of places where the function passed to .forEach() was async unnecessarily.

  5. Use a library: In a few places we could just replace the existing logic with a dependency which is already available and does the intended logic without any compromises.


Each helper implemented in this PR includes a variant which accepts a limit parameter between the iterable and the map function. I'm interested in having a discussion about if we should be using these instead of starting each concurrent iteration at the same time, potentially creating a lot more load from a single request on the server, or doing a lot more work on each frame in the UI. It's also possible that several places where I updated code take input from users which are variable in length and could be very long. In those cases I think it would make sense to limit the amount of work we are doing concurrently... idk, I originally wrote the "unlimited" helpers to have a default concurrency of 4 but got sheepish. For now the *WithLimit() versions are unused outside of the unlimited helpers, so if reviewers request I will remove them.

I could also imagine us requiring usage of such helpers instead of doing Promise.all(array.map()) which we do in many, many places. It could still be possible with something like asyncMapWithLimit(iter, Infinity, fn) but would require opting into this when the dev knows it won't lead to too much concurrency.

@spalger spalger force-pushed the implement/eslint-no-async-foreach branch from cb2684a to 9fb9c19 Compare September 9, 2021 15:28
@spalger spalger force-pushed the implement/eslint-no-async-foreach branch from 9fb9c19 to ee60f73 Compare September 9, 2021 15:34
@spalger spalger force-pushed the implement/eslint-no-async-foreach branch 3 times, most recently from 49e8f46 to 74adce4 Compare September 9, 2021 17:52
@spalger spalger force-pushed the implement/eslint-no-async-foreach branch from 74adce4 to 20495b0 Compare September 9, 2021 18:45
@spalger spalger added release_note:skip Skip the PR/issue when compiling release notes Team:Operations Team label for Operations Team v7.16.0 v8.0.0 labels Sep 13, 2021
@@ -92,5 +92,6 @@ module.exports = {
],

'@kbn/eslint/no_async_promise_body': 'error',
'@kbn/eslint/no_async_foreach': 'error',
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe we should expand the rule to cover other expressions, like this one #98463 (comment)
I'm not sure whether no-floating-promises implements the same check, but we can give it a try.

Copy link
Contributor Author

@spalger spalger Sep 13, 2021

Choose a reason for hiding this comment

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

Yeah, I'll look at a similar solution for that. I don't want to overload this rule though and have a special error message for that scenario.

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't want to overload this rule though and have a special error message for that scenario.

Would you suggest implementing a custom rule for every use-case when (...args: any[]) => Promise<any> is used instead of (...args: any[]) => void?

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 do think that having custom error messages for each situation would be nice, though I'm not convinced there are that many scenarios right now. It's possibly a bigger more wide issue than I'm thinking though.

return (
node.callee.type === esTypes.MemberExpression &&
node.callee.property.type === esTypes.Identifier &&
node.callee.property.name === 'forEach' &&
Copy link
Contributor

Choose a reason for hiding this comment

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

unfortunately, it doesn't cover cases when a promise is returned without an async keyword 😞

Copy link
Contributor Author

@spalger spalger Sep 13, 2021

Choose a reason for hiding this comment

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

Yeah, in that case we're creating a promise which can be handled, so I feel like it's less of an issue.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, in that case we're creating a promise which can be handled

Could you elaborate on it? Sorry, I'm not follow who handles a promise in this case 😞

async function doAsync () {}
[].forEach(doAsync)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, I figured you were describing:

array.forEach(() => new Promise((resolve) => ...))

There's a chance we could improve this to use types for locally defined functions, but the more research I do on other things the more I feel like we need to start providing ESLint with full types... Which has a lot of complications.

packages/kbn-std/src/iteration/test_helpers.ts Outdated Show resolved Hide resolved
packages/kbn-std/src/iteration/for_each.ts Show resolved Hide resolved
packages/kbn-std/src/index.ts Show resolved Hide resolved
@spalger spalger marked this pull request as ready for review September 13, 2021 23:36
@spalger spalger requested review from a team as code owners September 13, 2021 23:36
Copy link
Contributor

@YulNaumenko YulNaumenko left a comment

Choose a reason for hiding this comment

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

Alerting changes LGTM!

@botelastic botelastic bot added Feature:ExpressionLanguage Interpreter expression language (aka canvas pipeline) Team:Fleet Team label for Observability Data Collection Fleet team labels Sep 13, 2021
@elasticmachine
Copy link
Contributor

Pinging @elastic/fleet (Team:Fleet)

Copy link
Contributor

@jportner jportner left a comment

Choose a reason for hiding this comment

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

Security changes LGTM!

Copy link
Contributor

@flash1293 flash1293 left a comment

Choose a reason for hiding this comment

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

The timelion one scares me a bit, but I think this comment here

// In theory none of the args should ever be promises. This is probably a waste.
is also applicable for reduce and there's not actually something to resolve.

Tested and didn't notice any breakages, LGTM.

Copy link
Contributor

@shahzad31 shahzad31 left a comment

Choose a reason for hiding this comment

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

Uptime Changes Looks Good !!

Copy link
Contributor

@cjcenizal cjcenizal left a comment

Choose a reason for hiding this comment

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

Changes to Rollup telemetry collector LGTM, but this code is really the domain of @elastic/kibana-vis-editors since it's about visualizations that use rollups.

x-pack/plugins/rollup/server/collectors/helpers.ts Outdated Show resolved Hide resolved
@cjcenizal cjcenizal requested a review from a team September 14, 2021 15:49
Copy link
Contributor

@FrankHassanabad FrankHassanabad left a comment

Choose a reason for hiding this comment

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

LGTM,

  • Looked over 1 section of code, left one comment
  • Talked about it with the internal team.

@kibanamachine
Copy link
Contributor

💚 Build Succeeded

Metrics [docs]

Async chunks

Total size of all lazy-loaded chunks that will be downloaded as the user navigates the app

id before after diff
maps 3.2MB 3.2MB +81.0B
observability 550.2KB 550.3KB +101.0B
security 764.1KB 764.2KB +114.0B
visTypeVislib 547.2KB 547.4KB +106.0B
total +402.0B

Page load bundle

Size of the bundles that are downloaded on every page load. Target size is below 100kb

id before after diff
kbnUiSharedDeps-elastic 2.7MB 2.7MB +11.0B
kbnUiSharedDeps-js 6.5MB 6.5MB +2.3KB
maps 80.7KB 80.8KB +66.0B
observability 59.0KB 59.1KB +66.0B
security 84.3KB 84.3KB +66.0B
visTypeVislib 32.0KB 32.0KB +66.0B
total +2.5KB
Unknown metric groups

References to deprecated APIs

id before after diff
maps 1173 1176 +3

History

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

Copy link
Contributor

@thomasneirynck thomasneirynck left a comment

Choose a reason for hiding this comment

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

maps/* changes lgtm

🙇 asyncMap, so much cleaner.

Copy link
Member

@nchaulet nchaulet left a comment

Choose a reason for hiding this comment

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

Fleet tests changes 🚀

Copy link
Contributor

@mshustov mshustov left a comment

Choose a reason for hiding this comment

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

Ok for the core changes. I don't want to block this PR. The core team can refactor the tests in @kbn/std later.

Copy link
Contributor

@vadimkibana vadimkibana left a comment

Choose a reason for hiding this comment

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

bfetch code changes LGTM.

@spalger spalger added the auto-backport Deprecated - use backport:version if exact versions are needed label Sep 14, 2021
@spalger spalger merged commit 2976f33 into elastic:master Sep 14, 2021
@spalger spalger deleted the implement/eslint-no-async-foreach branch September 14, 2021 20:20
@kibanamachine
Copy link
Contributor

💔 Backport failed

Status Branch Result
7.x Commit could not be cherrypicked due to conflicts

To backport manually run:
node scripts/backport --pr 111637

spalger pushed a commit to spalger/kibana that referenced this pull request Sep 14, 2021
Co-authored-by: spalger <spalger@users.noreply.github.com>
# Conflicts:
#	x-pack/plugins/event_log/server/es/init.ts
spalger added a commit that referenced this pull request Sep 15, 2021
…2178)

* [eslint] add rule to forbid async forEach bodies (#111637)

Co-authored-by: spalger <spalger@users.noreply.github.com>
# Conflicts:
#	x-pack/plugins/event_log/server/es/init.ts

* remove unnecessary async forEach

Co-authored-by: spalger <spalger@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
auto-backport Deprecated - use backport:version if exact versions are needed Feature:ExpressionLanguage Interpreter expression language (aka canvas pipeline) release_note:skip Skip the PR/issue when compiling release notes Team:Fleet Team label for Observability Data Collection Fleet team Team:Operations Team label for Operations Team v7.16.0 v8.0.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.