Share the query pool, so concurrency actually limits - #297
dougdonohoe wants to merge 2 commits into
Conversation
`queryREST` and `queryGraphQL` resolve their pool lazily:
opts.pool ??= new PromisePool(opts.concurrency)
But every caller reaches them through a spread. `getGitHubRepository`, for
example, calls `queryREST({ ...opts, pathname })`, and there are over twenty
such sites. The assignment therefore lands on a throwaway copy of the options,
so each request builds a pool of its own and nothing limits the batch. The
`concurrency` option silently limits nothing.
Keying pools by concurrency at module level fixes every one of those call
sites at once, since they all funnel through these two functions. An explicitly
supplied `pool` still wins, and a concurrency of 0 still means unlimited.
Peak in-flight requests through `getGitHubRepositories` over 430 slugs, with a
stubbed fetch:
before after
no options 430 100
{ concurrency: 10 } 430 10
{ concurrency: 0 } 430 430
Note the behavior change in the first row. Previously an unspecified
concurrency produced `new PromisePool(undefined)`, which is unlimited. It now
defaults to 100, which is the ceiling GitHub documents for concurrent requests,
shared across the REST and GraphQL APIs. Callers wanting the old behavior can
pass `concurrency: 0`.
Adds a `pool` suite covering this. The behavioral test drives `queryREST`
against a stubbed fetch, so it needs neither the network nor credentials, and
it fails against the unfixed build.
|
@balupton - have you had a chance to look at this? |
|
I eyeballed it last week. Planning to clone it out in the next 24 hours, and test it out. Next window to work on PRs will be mid next week. |
|
Will have to be next Wednesday. This week is too busy with upcoming travel. Sorry for the delay |
|
integrating now |
|
Strange. I tried to merge this via the CLI and it didn't work properly. Anyway, it is merged and you are credited. Release incoming. My workflow was |
also makes use of bevry/github-api#297 this has uncovered a bug where ETIMEDOUT is happening despite not timing out, this turns out to be some type of node.js gotcha, debugging
update github-api which now handles concurrency correctly thanks to @dougdonohoe and bevry/github-api#297
Requested by @balupton in bevry/staticsitegenerators#497:
This is the first of those two. The rate-limit retries are #280, which this deliberately does not touch.
The bug
queryRESTandqueryGraphQLresolve their pool lazily:But every caller reaches them through a spread —
getGitHubRepositorycallsqueryREST({ ...opts, pathname }), and there are over twenty such sites. The assignment lands on a throwaway copy of the options, so each request constructs a pool of its own and nothing limits the batch.The visible symptom is that
concurrencysilently limits nothing. In staticsitegenerators,getGitHubRepositoriesover 430 slugs issued all 430 requests simultaneously and tripped GitHub's secondary rate limit, and passing{ concurrency: 10 }changed nothing.The change
Keying pools by concurrency at module level fixes every call site at once, since they all funnel through those two functions. An explicitly supplied
poolstill wins, andconcurrency: 0still means unlimited.Peak in-flight requests through
getGitHubRepositoriesover 430 slugs, with a stubbed fetch:{ concurrency: 10 }{ concurrency: 0 }Behavior change, flagged deliberately
That first row is a change for existing consumers. Previously an unspecified concurrency produced
new PromisePool(undefined), andnative-promise-pooltreats0as unlimited — so the default was no limit at all. It now defaults to 100, which is the ceiling GitHub documents:Callers who want the previous behavior can pass
concurrency: 0. Happy to dropdefaultConcurrencyback to unlimited and fix only the sharing if you would rather not change the default in a patch — the two parts are independent.Tests
Adds a
poolsuite. The behavioral test drivesqueryRESTagainst a stubbedfetch, so it needs neither the network nor credentials and runs in CI regardless of secrets. Verified it fails against the unfixed build:The other three cover the registry contract: spread options resolve to the same pool, different concurrencies resolve to different pools, and an unspecified concurrency resolves to the documented default.
tsc,eslintandprettierare clean.One unrelated thing worth knowing
Running
npm run our:compileon a machine without deno silently rewritespackage.json, removing the deno edition entry and its keywords —make-deno-edition --attemptreportsnecessary entry file [source/index.ts] failedand drops it rather than failing the build. It is easy for a contributor to commit that by accident and delete the deno edition from the published package. I have kept it out of this PR.