plugin-throttling.js icon indicating copy to clipboard operation
plugin-throttling.js copied to clipboard

`retryCount` randomly has a wrong value

Open yoanmLf opened this issue 3 years ago • 6 comments

TLDR; In case a request hit the rate limit, plugin does its job 👌 . But if a second request also hit the rate limit (either for token 2 or because that request also uses token 1) while other one is waiting, my rate-limiter listener receives a options.request.retryCount at 1 instead of 0 So request is not retried, the HTTP error is thrown, and the whole process stop.


Hello,

I have a really strange behavior with the plugin, I wonder if it comes from the fact I use 2 different clients or the fact to use Promise.all 🤔 Each client has a dedicated token, and each function using github api use a dedicated client (looping from first to the second, then first again, then second again, etc)

When I receive an HTTP 403 error :

  • plugin is correctly called
  • plugin correctly call rate-limiter listener
  • my rate-limiter listener asks for retry

At this point, everything works. In case I hit the rate limit at the beginning of the process, there is no issue, request is retried correctly after the retryAfter time.

But there is cases where I call a methods using octokit client and push the resulting promise inside an array and do that X times. Then I await a Promise.all of the array of promise. As said before, each call will use a dedicated client (first or second) and so a dedicated token. In case a request hit the rate limit, plugin does its job 👌 . But if a second request also hit the rate limit (either for token 2 or because that request also uses token 1) while other one is waiting, my rate-limiter listener receives a options.request.retryCount at 1 instead of 0 So request is not retried, the HTTP error is thrown, and the whole process stop.

Do you know if plugin works in that configuration ?

N.B. :

  • I already tried to set an id for each instance under throttling key => no change
  • I tried to inject a dedicated Bottleneck light instance (using throttling.retryLimiter key) => no change
  • I tried to set an unique ID for each Bottleneck light instance too => no change

Packages versions :

  • @octokit/auth-token=>2.4.5
  • @octokit/core=>3.5.1
  • @octokit/endpoint=>6.0.12
  • @octokit/graphql=>4.8.0
  • @octokit/openapi-types=>9.4.0
  • @octokit/plugin-paginate-rest=>2.6.0
  • @octokit/plugin-request-log=>1.0.2
  • @octokit/plugin-rest-endpoint-methods=>4.2.1
  • @octokit/plugin-throttling=>3.5.1 Root requirement
  • @octokit/request=>5.6.1
  • @octokit/request-error=>2.1.0
  • @octokit/rest=>18.0.9 Root requirement
  • @octokit/types=>6.24.0 Root requirement
  • bottleneck=>2.19.5 Root requirement

Current implementation :

import { throttling } from '@octokit/plugin-throttling';
import { Octokit } from '@octokit/rest'; // <-- Not @octokit/core, like on your doc
import BottleneckLight from 'bottleneck/light';
import * as uuid from 'uuid';

type RetryOptions = {
  method: string;
  url: string;
  request: {
    retryCount: number;
  };
};

const tokens = {token: 'XXXXXX', token1: 'YYYYYYYY'};
const baseThrottleOptions = {
  onRateLimit: (retryAfter: number, options: RetryOptions): null | true => {
    const context = {
      errorType,
      method: options.method,
      url: options.url,
      retryCount: options.request.retryCount,
      retryAfter,
    };
    console.warn(context, `Throttle plugin error : ${errorType}`);

    if (options.request.retryCount < 1) {
      // only retries once
      console.info(context, `Retrying request !`);

      return true;
    }

    console.error(context, `Request already retried, giving up`);

    return null;
},
  onAbuseLimit: .... // Same basic listener as above
};

const gitHubClients = Object.keys(tokens).map((key) => {
  const token = tokens[key];
  const CustomOctokit = Octokit.plugin(throttling);
  const id = `${uuid()}-ID>>${key}<<`;
  const instance = new CustomOctokit({
    auth: token,
    throttle: {
      ...baseThrottleOptions,
      id,
      retryLimiter: new BottleneckLight({ id: `${id}-BottleneckLight` }),
    },
  });

  return instance;
});

I also patched node_modules lib files in order to add ids and debug statements everywhere, see below what I found so far (FYI: all of this is made within the same second):

  1. Bottleneck job <no-id>-yy2p6xzj4eb Bottleneck queue QUEUE-uzvp13sxylo
 Bottleneck instance octokit-global-427913a9-c793-406d-9964-3999912f835d-ID>>token1<<

    • I guess it the Bottleneck instance created by octokit (not by your plugin)

    x-github-request-id for failing response: A16A:58C8:D89F80:E50A53:613F4287 Url of failing request : https://api.github.com/repos/OWNER/REPO_1/contents/.github/PULL_REQUEST_TEMPLATE

==> No listener call (Not enough time to execute listener OR is master job of Bottleneck job <no-id>-lpreittunul ?)

  1. Bottleneck job <no-id>-crqa4hcsngi Bottleneck queue QUEUE-uzvp13sxylo Bottleneck instance octokit-global-427913a9-c793-406d-9964-3999912f835d-ID>>token1<< x-github-request-id for failing response: 8431:F763:FBB74:1010CA:613F53B9 Url of failing request : https://api.github.com/repos/OWNER/REPO_1/contents/pull_request_template.md

==> No listener call (Not enough time to execute listener OR is master job of Bottleneck job <no-id>-gz4915z0qbl ?)

  1. Bottleneck job <no-id>-lpreittunul Bottleneck queue QUEUE-xa5jdp0wx2c Bottleneck instance 427913a9-c793-406d-9964-3999912f835d-ID>>token1<<-BottleneckLight x-github-request-id for failing response: DA11:CC27:103F7E:109607:613F53B9 Url of failing request : https://api.github.com/repos/OWNER/REPO_1/contents/.github/PULL_REQUEST_TEMPLATE

=> Listener call OK, ask for retry

  1. Bottleneck job <no-id>-gz4915z0qbl Bottleneck queue QUEUE-xa5jdp0wx2c Bottleneck instance 427913a9-c793-406d-9964-3999912f835d-ID>>token1<<-BottleneckLight x-github-request-id for failing response: 8431:F763:FBB74:1010CA:613F53B9 Url of failing request : https://api.github.com/repos/OWNER/REPO_1/contents/pull_request_template.md

=> Listener call OK, give up as retryCount=1

As you can see, both requests use the same token (>>token1<<), so it makes sens that second one fails. But I would expect the second request to have a retryCount at 0, not 1 BTW, the retryCount value of Bottleneck job is 0 at this point, but listener can't access that value

yoanmLf avatar Sep 13 '21 15:09 yoanmLf

In case it helps, see below patch for the two node_module lib files: tmp/node_modules/xxx is used to replace the corresponding file when docker image is created

@octokit/plugin-throttling/dist-node/index.js

index 0ccf157..272a860 100644
--- a/node_modules/@octokit/plugin-throttling/dist-node/index.js
+++ b/tmp/node_modules/@octokit/plugin-throttling/dist-node/index.js
@@ -184,6 +184,7 @@ const createGroups = function (Bottleneck, common) {
 };
 
 function throttling(octokit, octokitOptions = {}) {
+  console.log('DEBUG - PLUGIN: throttling(octokit, octokitOptions = {})');
   const {
     enabled = true,
     Bottleneck = BottleneckLight,
@@ -195,6 +196,7 @@ function throttling(octokit, octokitOptions = {}) {
   } = octokitOptions.throttle || {};
 
   if (!enabled) {
+    console.log(`DEBUG - PLUGIN (${id}): Disabled !!!`);
     return {};
   }
 
@@ -212,11 +214,13 @@ function throttling(octokit, octokitOptions = {}) {
       triggersNotification,
       minimumAbuseRetryAfter: 5,
       retryAfterBaseValue: 1000,
-    retryLimiter: new Bottleneck(),
+      retryLimiter: new Bottleneck({id: 'DEFAULT'}),
       id
     }, groups), // @ts-ignore
     octokitOptions.throttle);
 
+  console.log(`DEBUG - PLUGIN (${id}): Bottleneck ID "${state.retryLimiter.id}"`);
+
   if (typeof state.onAbuseLimit !== "function" || typeof state.onRateLimit !== "function") {
     throw new Error(`octokit/plugin-throttling error:
         You must pass the onAbuseLimit and onRateLimit error handlers.
@@ -237,10 +241,20 @@ function throttling(octokit, octokitOptions = {}) {
   events.on("abuse-limit", state.onAbuseLimit); // @ts-ignore
 
   events.on("rate-limit", state.onRateLimit); // @ts-ignore
+  events.on("debug", (msg, infos) => {
+    console.log(`DEBUG - PLUGIN (${id}): DEBUG EVENT =>`);
+    console.log({msg, infos});
+  }); // @ts-ignore
 
   events.on("error", e => console.warn("Error in throttling-plugin limit handler", e)); // @ts-ignore
 
+  console.log(`DEBUG - PLUGIN (${id}): INSTALL LISTENERS`);
   state.retryLimiter.on("failed", async function (error, info) {
+    console.log(
+      `DEBUG - PLUGIN (${id}): ON FAILED`,
+      {error, info, options: info.args[info.args.length - 1], request: info.args[info.args.length - 1].request, response: error.response}
+    );
+
     const options = info.args[info.args.length - 1];
     const {
       pathname
@@ -248,6 +262,8 @@ function throttling(octokit, octokitOptions = {}) {
     const shouldRetryGraphQL = pathname.startsWith("/graphql") && error.status !== 401;
 
     if (!(shouldRetryGraphQL || error.status === 403)) {
+      console.log(`DEBUG - PLUGIN (${id}): ARGHH !!! EXIT 1`);
+
       return;
     }
 
@@ -257,7 +273,9 @@ function throttling(octokit, octokitOptions = {}) {
       wantRetry,
       retryAfter
     } = await async function () {
+      console.log(`DEBUG - PLUGIN (${id}): EXECUTE CALLBACKS`);
       if (/\babuse\b/i.test(error.message)) {
+        console.log(`DEBUG - PLUGIN (${id}): EXECUTE ABUSE CALLBACK`);
         // The user has hit the abuse rate limit. (REST and GraphQL)
         // https://docs.github.com/en/rest/overview/resources-in-the-rest-api#abuse-rate-limits
         // The Retry-After header can sometimes be blank when hitting an abuse limit,
@@ -271,6 +289,7 @@ function throttling(octokit, octokitOptions = {}) {
       }
 
       if (error.response.headers != null && error.response.headers["x-ratelimit-remaining"] === "0") {
+        console.log(`DEBUG - PLUGIN (${id}): EXECUTE RATE LIMIT CALLBACK`);
         // The user has used all their allowed calls for the current time period (REST and GraphQL)
         // https://docs.github.com/en/rest/reference/rate-limit (REST)
         // https://docs.github.com/en/graphql/overview/resource-limitations#rate-limit (GraphQL)
@@ -286,12 +305,14 @@ function throttling(octokit, octokitOptions = {}) {
       return {};
     }();
 
+    console.log(`DEBUG - PLUGIN (${id}): wantRetry : `+ (wantRetry ? 'T' : 'F'));
     if (wantRetry) {
       options.request.retryCount++; // @ts-ignore
 
       return retryAfter * state.retryAfterBaseValue;
     }
   });
+  console.log(`DEBUG - PLUGIN (${id}): INSTALL LISTENERS - OK`);
   octokit.hook.wrap("request", wrapRequest.bind(null, state));
   return {};
 }

node_modules/bottleneck/light.js

index c4aa265..9150c17 100644
--- a/node_modules/bottleneck/light.js
+++ b/tmp/node_modules/bottleneck/light.js
@@ -231,7 +231,9 @@
   Events$1 = Events_1;
 
   Queues = class Queues {
-	  constructor(num_priorities) {
+    constructor(num_priorities, debugId='UNKNOWN') {
+      this.debugId = debugId;
+      console.log(`DEBUG - BOTTLENECK QUEUE (${this.debugId}): CONSTRUCT`);
       var i;
       this.Events = new Events$1(this);
       this._length = 0;
@@ -262,6 +264,7 @@
     }
 
     push(job) {
+      console.log(`DEBUG - BOTTLENECK QUEUE (${this.debugId}): PUSH JOB "${job.options.id}"`);
       return this._lists[job.options.priority].push(job);
     }
 
@@ -402,14 +405,19 @@
       eventInfo = {args: this.args, options: this.options, retryCount: this.retryCount};
       this.Events.trigger("executing", eventInfo);
       try {
+        console.log(`DEBUG - BOTTLENECK JOB (${this.options.id}): PASSED ?`);
         passed = (await (chained != null ? chained.schedule(this.options, this.task, ...this.args) : this.task(...this.args)));
+        console.log(`DEBUG - BOTTLENECK JOB (${this.options.id}): PASSED OK`);
         if (clearGlobalState()) {
+          console.log(`DEBUG - BOTTLENECK JOB (${this.options.id}): clearGlobalState()`);
           this.doDone(eventInfo);
           await free(this.options, eventInfo);
           this._assertStatus("DONE");
           return this._resolve(passed);
         }
+        console.log(`DEBUG - BOTTLENECK JOB (${this.options.id}): HUMMMMM ?????`);
       } catch (error1) {
+        console.log(`DEBUG - BOTTLENECK JOB (${this.options.id}): ERROR 1 !!!!`, {error1})
         error = error1;
         return this._onFailure(error, eventInfo, clearGlobalState, run, free);
       }
@@ -427,15 +435,20 @@
     }
 
     async _onFailure(error, eventInfo, clearGlobalState, run, free) {
+      console.log(`DEBUG - BOTTLENECK JOB (${this.options.id}): _onFailure`)
       var retry, retryAfter;
       if (clearGlobalState()) {
+        console.log(`DEBUG - BOTTLENECK JOB (${this.options.id}): listenerCount = `+(this.Events.listenerCount("failed")));
         retry = (await this.Events.trigger("failed", error, eventInfo));
+        console.log(`DEBUG - BOTTLENECK JOB (${this.options.id}): retry = `+(retry != null ? 'T' : 'F'));
         if (retry != null) {
+          console.log(`DEBUG - BOTTLENECK JOB (${this.options.id}): retry -> OK`);
           retryAfter = ~~retry;
           this.Events.trigger("retry", `Retrying ${this.options.id} after ${retryAfter} ms`, eventInfo);
           this.retryCount++;
           return run(retryAfter);
         } else {
+          console.log(`DEBUG - BOTTLENECK JOB (${this.options.id}): retry -> ARGHHH !`);
           this.doDone(eventInfo);
           await free(this.options, eventInfo);
           this._assertStatus("DONE");
@@ -1063,7 +1076,8 @@
         this._addToQueue = this._addToQueue.bind(this);
         this._validateOptions(options, invalid);
         parser$5.load(options, this.instanceDefaults, this);
-	      this._queues = new Queues$1(NUM_PRIORITIES$1);
+        this.queueDebugId = 'QUEUE-'+Math.random().toString(36).slice(2);
+        this._queues = new Queues$1(NUM_PRIORITIES$1, this.queueDebugId);
         this._scheduled = {};
         this._states = new States$1(["RECEIVED", "QUEUED", "RUNNING", "EXECUTING"].concat(this.trackDoneStatus ? ["DONE"] : []));
         this._limiter = null;
@@ -1090,6 +1104,7 @@
           var ref;
           return (ref = this._store.heartbeat) != null ? typeof ref.unref === "function" ? ref.unref() : void 0 : void 0;
         });
+        console.log(`DEBUG - BOTTLENECK (${this.id} / QUEUE ID=${this.queueDebugId}): CONSTRUCT`);
       }
 
       _validateOptions(options, invalid) {
@@ -1382,6 +1397,7 @@
           });
         };
         job = new Job$1(task, args, options, this.jobDefaults, this.rejectOnDrop, this.Events, this._states, this.Promise);
+        console.log(`DEBUG - BOTTLENECK (${this.id} / QUEUE ID=${this.queueDebugId}): submit -> Created Job "${job.options.id}"`);
         job.promise.then(function(args) {
           return typeof cb === "function" ? cb(...args) : void 0;
         }).catch(function(args) {
@@ -1403,6 +1419,7 @@
           [options, task, ...args] = args;
         }
         job = new Job$1(task, args, options, this.jobDefaults, this.rejectOnDrop, this.Events, this._states, this.Promise);
+        console.log(`DEBUG - BOTTLENECK (${this.id} / QUEUE ID=${this.queueDebugId}): schedule -> Created Job "${job.options.id}"`);
         this._receive(job);
         return job.promise;
       }

yoanmLf avatar Sep 13 '21 15:09 yoanmLf

Does it work with multiple parallel calls and/or multiple clients

Short answer, yes :)

Do the tokens belong to the same user account / app? If so, then you have to pass a single bottleneck instance to all of them, that way the throttling will work across the instances.

But if a second request also hit the rate limit (either for token 2 or because that request also uses token 1) while other one is waiting, my rate-limiter listener receives a options.request.retryCount at 1 instead of 0 So request is not retried, the HTTP error is thrown, and the whole process stop.

Do you know if plugin works in that configuration ?

There might be a bug with retryCount, but it's hard to say with your code, we would need to isolate the problem further.

gr2m avatar Sep 13 '21 15:09 gr2m

Thanks for the quick answer :)

Do the tokens belong to the same user account / app? If so, then you have to pass a single bottleneck instance to all of them, that way the throttling will work across the instances.

Nop, tokens belong to two different users (it was also my guess at beginning, but I double checked and it's not the case).

There might be a bug with retryCount, but it's hard to say with your code, we would need to isolate the problem further.

I guess yes, but I spent 3 days on that issue without being able to figure out the root cause. I know where the plugin increase that value https://github.com/octokit/plugin-throttling.js/blob/c9c3bf68c663889e36389d170746db6dfe86cbb8/src/index.ts#L168-L172 options comes from https://github.com/octokit/plugin-throttling.js/blob/c9c3bf68c663889e36389d170746db6dfe86cbb8/src/index.ts#L110-L111 info comes from that call on Bottleneck Job (eventInfo)

retry = (await this.Events.trigger("failed", error, eventInfo));

which is a function arg

async _onFailure(error, eventInfo, clearGlobalState, run, free) {

which comes from that call (at least from what I saw during debug session)

return this._onFailure(error, eventInfo, clearGlobalState, run, free);

and is built there

eventInfo = {args: this.args, options: this.options, retryCount: this.retryCount};

this.arg is a class argument

	Job = class Job {
	  constructor(task, args, options, jobDefaults, rejectOnDrop, Events, _states, Promise) {
	    this.task = task;
	    this.args = args;

and seems (start to be hard to follow things from there :|) to come from there or there In both cases, I don't understand at all how one job instance may impact another job instance args 🤔 Except if for a reason or another, request is overridden somewhere during the process.

I will see if I can create a minimal snippet which reproduces the issue and will post it there (it will depend of time required, I already spent many time and can't afford to spend too much work day on that :/)

yoanmLf avatar Sep 14 '21 08:09 yoanmLf

How to reproduce the issue :

  • index.js

     const { throttling } = require('@octokit/plugin-throttling');
     const { Octokit } = require('@octokit/rest');
    
     const repoOwner = 'XXXXX';
     const repoName = 'YYYYY';
    
     const beforeRequestHook = async (options) => {
       console.log(
         {
           // Sometimes url already contains the base, sometimes not :|
           call: `${options.method} ${options.baseUrl}${options.url.replace(options.baseUrl, '')}`,
         },
         'Octokit - request will be sent',
       );
     };
     const afterRequestHook = async (options) => {
       // FYI: Will be called only if request did not throw an error (or if error was caught)
       console.log({ status: options.status, url: options.url }, 'Octokit - request has been sent');
     };
    
    
     const CustomOctokit = Octokit.plugin(throttling);
     const api = new CustomOctokit({
     //  auth: 'token',
       throttle: {
         onRateLimit: (retryAfter, options) => {
           const context = { method: options.method, url: options.url, retryCount: options.request.retryCount, retryAfter };
           console.warn(context, `Throttle plugin error : Rate limit`);
    
           if (options.request.retryCount < 1) {
             // only retries once
             console.log(context, `Retrying request !`);
    
             return true;
           }
    
           console.error(context, `Request already retried, giving up`);
    
           return null;
         },
         onAbuseLimit: (retryAfter, options) => {
           console.warn(context, `Throttle plugin error : Abuse`);
    
           return null;
         },
       },
     });
     api.hook.before('request', beforeRequestHook);
     api.hook.after('request', afterRequestHook);
    
    
    
     async function doApiCall(ref) {
       try {
         // Will always fail as filename is random !
         return await api.request(`GET /repos/${repoOwner}/${repoName}/contents/${ref}-${Math.random().toString(36).slice(2)}`)
       } catch(e) {
         if (!e.status || e.status !== 404) {
           throw e;
         }
    
         return null;
       }
     }
    
     async function doParallelApiCalls(ref) {
       return Promise.all([doApiCall(ref), doApiCall(ref), doApiCall(ref)]);
     }
    
     const asyncIterable = {
       [Symbol.asyncIterator]() {
         return {
           i: 0,
           async next() {
             if (this.i < 100) {
               this.i++;
               return Promise.resolve({ value: await doParallelApiCalls(this.i), done: false });
             }
    
             return Promise.resolve({ done: true });
           }
         };
       }
     };
    
     (async function() {
       for await (let num of asyncIterable) {
         console.log(num);
       }
     })();
    
  • package.json:

     {
       "dependencies": {
         "@octokit/plugin-throttling": "^3.5.1",
         "@octokit/rest": "^18.0.9",
         "bottleneck": "^2.19.5"
       }
     }
    

Then

  • npm install
  • node index.js

Issue seems to be totally random, you have to execute node index.js many times, sometimes it works without issue, and sometimes it fails.

Working example output :

{ call:
   'GET https://api.github.com/repos/XXXXX/YYYYY/contents/1-sa7x94o7b4q' } 'Octokit - request will be sent'
{ call:
   'GET https://api.github.com/repos/XXXXX/YYYYY/contents/1-pzamkghgus' } 'Octokit - request will be sent'
{ call:
   'GET https://api.github.com/repos/XXXXX/YYYYY/contents/1-uu1srtxc5xi' } 'Octokit - request will be sent'
{ method: 'GET',
  url: '/repos/XXXXX/YYYYY/contents/1-uu1srtxc5xi',
  retryCount: 0, <--- OK !
  retryAfter: 1867 } 'Throttle plugin error : Rate limit'
{ method: 'GET',
  url: '/repos/XXXXX/YYYYY/contents/1-uu1srtxc5xi',
  retryCount: 0, <--- OK !
  retryAfter: 1867 } 'Retrying request !'
{ method: 'GET',
  url: '/repos/XXXXX/YYYYY/contents/1-pzamkghgus',
  retryCount: 0, <--- OK !
  retryAfter: 1867 } 'Throttle plugin error : Rate limit'
{ method: 'GET', <--- OK !
  url: '/repos/XXXXX/YYYYY/contents/1-pzamkghgus',
  retryCount: 0,
  retryAfter: 1867 } 'Retrying request !'
{ method: 'GET', <--- OK !
  url: '/repos/XXXXX/YYYYY/contents/1-sa7x94o7b4q',
  retryCount: 0, <--- OK !
  retryAfter: 1867 } 'Throttle plugin error : Rate limit'
{ method: 'GET',
  url: '/repos/XXXXX/YYYYY/contents/1-sa7x94o7b4q',
  retryCount: 0, <--- OK !
  retryAfter: 1867 } 'Retrying request !'}

..... Process waits correctly until retryAfter is elapsed

Failing example output:

{ call:
   'GET https://api.github.com/repos/XXXXX/YYYYY/contents/1-2zyky93cmm7' } 'Octokit - request will be sent'
{ call:
   'GET https://api.github.com/repos/XXXXX/YYYYY/contents/1-mlapmrmt36' } 'Octokit - request will be sent'
{ call:
   'GET https://api.github.com/repos/XXXXX/YYYYY/contents/1-6cuv8zce0pw' } 'Octokit - request will be sent'
{ method: 'GET',
  url: '/repos/XXXXX/YYYYY/contents/1-6cuv8zce0pw',
  retryCount: 0, <--- OK !
  retryAfter: 1839 } 'Throttle plugin error : Rate limit'
{ method: 'GET',
  url: '/repos/XXXXX/YYYYY/contents/1-6cuv8zce0pw',
  retryCount: 0, <--- OK !
  retryAfter: 1839 } 'Retrying request !'
{ method: 'GET',
  url: '/repos/XXXXX/YYYYY/contents/1-2zyky93cmm7',
  retryCount: 1, <--- Argh !
  retryAfter: 1839 } 'Throttle plugin error : Rate limit'
{ method: 'GET',
  url: '/repos/XXXXX/YYYYY/contents/1-2zyky93cmm7',
  retryCount: 1, <--- Argh !
  retryAfter: 1839 } 'Request already retried, giving up'
{ method: 'GET',
  url: '/repos/XXXXX/YYYYY/contents/1-mlapmrmt36',
  retryCount: 1, <--- Argh !
  retryAfter: 1839 } 'Throttle plugin error : Rate limit'
{ method: 'GET',
  url: '/repos/XXXXX/YYYYY/contents/1-mlapmrmt36',
  retryCount: 1, <--- Argh !
  retryAfter: 1839 } 'Request already retried, giving up'
(node:25114) UnhandledPromiseRejectionWarning: HttpError: API rate limit exceeded for [REDACTED]. (But here's the good news: Authenticated requests get a higher rate limit. Check out the documentation for more details.)
    at fetch.then ([REDACTED]/node_modules/@octokit/request/dist-node/index.js:86:21)
    at process._tickCallback (internal/process/next_tick.js:68:7)
(node:25114) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
(node:25114) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

yoanmLf avatar Sep 14 '21 09:09 yoanmLf

Thanks a lot for the thorough research!

I'm fully occupied with https://github.com/octokit/octokit-next.js right now and for the foreseeable time, but I'll get back to it eventually. I don't have an idea what the problem might be out of head unfortunately.

Maybe @SGrondin who created Bottleneck and the initial version of this plugin might have ideas how to further debug the problem, but Simon moved on from Bottleneck I think, he has not been responsive recently.

gr2m avatar Sep 14 '21 16:09 gr2m

No problem 👍 For the moment, we keep the plugin active knowing that it may not work in some cases. At least it delays requests which is a good thing anyway (in worst case, behavior will be the same than without the plugin, so let takes advantage of the other functionalities 😀 ).

BTW, thanks for your work 👌

yoanmLf avatar Sep 15 '21 07:09 yoanmLf

:tada: This issue has been resolved in version 4.3.2 :tada:

The release is available on:

Your semantic-release bot :package::rocket:

github-actions[bot] avatar Oct 31 '22 21:10 github-actions[bot]