【问题标题】:How to throttle typescript functions that return output如何限制返回输出的打字稿函数
【发布时间】:2018-06-14 10:26:00
【问题描述】:

我正在使用 typescript 编写一个 node.js 应用程序。我的应用程序将有多个相互通信的服务。一些服务需要调用外部 API。此 API 对每秒可以执行的调用次数有限制。因此,我想创建一个包装外部 API 调用的服务(我们称之为 ApiService)。其他服务将调用此服务,它将在队列中收集它们的请求并按顺序执行它们 - 每秒 N 个请求(为简单起见,我们假设每秒 1 个)。当服务 A 调用 ApiService 的方法时 - 它期望接收输出(可以接收 Promise)。

现在我的问题是 - 如何在 ApiService 中对这些 API 调用进行排队,以便每 1 秒执行一次队列中的下一个调用,并将该 API 调用的输出返回给 ApiService 的调用者?

这是一个示例服务:

export class ServiceA {
   apiService: ApiService;

   public constructor(_apiService: ApiService) {
      apiService = _apiService;
   }

   public async DoWork() {
      // Do some stuff
      const output: number = await apiService.RetrieveA(param1, param2);
      // Do something with the output
   }
}

ApiService:

export class ApiService {
   queue: (() => Promise<any>)[] = [];

   public async RetrieveA(param1, param2): Promise<number> {
      const func = async () => {
         return this.CallApi(param1, param2);
      };

      this.queue.push(func);
      return func();
   }

   public async RunQueue() {
      while(true) {
         const func = this.queue.shift();
         if (!func) { continue; }
         // Call the function after 1 second
         await setTimeout(() => { func(); }, 1000);
      }
   }

   private async CallApi(param1, param2): Promise<number> {
      // Call the external API, process its output and return
   }
}

编排整个事情的主要方法:

var CronJob = require('cron').CronJob;

const apiService = new ApiService();
const service = new ServiceA(apiService);

new CronJob('* * * * * *', function() {
   service.DoWork();
}, null, true);

apiService.RunQueue();

我面临的问题是,当 RetrieveA 方法返回 func() - 函数被执行。我需要返回一个 Promise,但实际的函数执行需要在 RunQueue() 方法中进行。有没有办法做到这一点?我可以在不立即执行该函数的情况下返回一个 Promise 并等待该 Promise - 在 RunQueue 方法中调用该函数时接收输出吗?

或者是否有其他方法可以解决我限制返回输出的 API 调用的问题?

我是 Node.js/Typescript/JavaScript 世界的新手,因此感谢您提供任何帮助 :)

【问题讨论】:

  • 如果您有很多项目要异步结果,那么following answer 可能会有所帮助。您可以限制每个周期的活动承诺或承诺数量(例如每秒不超过 10 个)。如果您确实有大量数据要处理,那么该代码会显示您将其分块至每批 1000 个。您可以使用stream 作为数据源,将其批量化为每批 1000 个,并限制活动承诺的数量。
  • 不确定我是否完全理解您的建议。我不知道如何将 Promise 返回给我的 ApiService 的调用者,但在任意时间点执行计算它的方法。另外我没有很多项目,例如每 10 秒大约 30 个,对我来说重要的是我每秒执行不超过 2 个。

标签: javascript node.js typescript throttling delayed-execution


【解决方案1】:

如果您想将对 RetreiveA 的调用限制为每秒 2 次,所有这些都可以简单得多:

//lib is here: https://github.com/amsterdamharu/lib/blob/master/src/index.js
import * as lib from '../../src/index'
const twoPerSecond = lib.throttlePeriod(2,1000);
export class ApiService {
  public RetrieveA(param1, param2): Promise<number> {
    //removed the resolver part, according to the typescript signature
    //  it should return a promise of number but resolver actually takes
    //  that number and returns void (undefined?)
    return twoPerSecond(this.CallApi.bind(this))([param1, param2]);
  }
  //change the signature of this function to take one parameter
  //  but deconsruct the array to param1 and param2
  private async CallApi([param1, param2]): Promise<number> {
    // Call the external API, process its output and return
  }
}

只有当这个类只有一个实例时,你的方法才有效。如果您要创建多个实例并在这些实例上调用 RetrieveA,则您不再将请求限制为 callApi

【讨论】:

  • 是的,但我确实打算只拥有这个类的一个实例(并将它作为参数传递给将使用它的所有其他服务):) 你的建议会奏效,但似乎只有如果我有一个 RetrieveA 方法。但是我需要有多种方法(例如 RetrieveB、RetrieveC...)——所有这些方法都会调用外部 API 的不同方法。但是我的限制是每秒不超过 2 个 API 调用(不管 API 调用是什么)。所以我需要同步对所有方法的调用,而不仅仅是一个方法。
  • @dbarfonchovski 您可以通过不直接调用 api:someApi(param1,param2) 而是使用 twoPerSecondtwoPerSecond(someApi)([param1,param2]) 来摆脱队列和​​所有这些,这需要您的 api 函数签名更改为接收一个数组(C# 中的元组)并解构它。或者您可以像这样调用 api:twoPerSecond(([param1,param2])=&gt;someApi(param1,param2))([param1,param2]) 因为您使用的是类,并且可能是 this,您很快就会发现这是一种可怕的做事方式:youtu.be/DePE0ffiMf4?t=44m40s
  • 感谢您的澄清,但我仍然无法理解如何限制对多个 API 方法的调用。也许我错过了你的观点,或者我没有很好地解释我的问题。想象一下外部 API 有 3 个方法 - GetAGetBGetC。使用twoPerSecond,我将能够将对GetA 的调用限制为每秒2 个。但是在那 1 秒内 - 我不希望执行对 GetBGetC 的调用。 API 限制为每秒 2 次调用,与 API 方法无关。因此,如果在 1 秒内我向 GetA 发送 2 个电话,向 GetB 发送 1 个电话 - 这将导致错误。
  • @dbarfonchovski 只有一个twoPerSecond 函数,无论您传递给twoPerSecond 的其他函数是什么,它每秒只会启动2 个。所以你可以这样做:twoPerSecond(this.GetA.bind(this)([param1,param2]) 然后twoPerSecond(this.GetB.bind(this)([param1,param2]) 然后twoPerSecond(this.GetC.bind(this)([param1,param2])。第一秒 GetA 和 GetB 将启动,下一秒 GetC 将启动。
  • @dbarfonchovski twoPerSecond 函数包含您现在在每个方法和类中展开的逻辑。函数上的comment 表示,如果在 period 内已经调用了超过 max 的函数,则不会调用传递给该函数的函数,该示例显示了相同的函数,但传递的函数实际上并不重要(只要它返回一个承诺)。
【解决方案2】:

我确实设法找到了一个可行的解决方案。我对 JavaScript 中的整个 Promise 和 async 概念不是很熟悉,所以这可能不是最好的解决方案,但它可以满足我的具体情况。以下是其他希望实现类似功能的人的代码:

示例 ServiceA 与上面相同:

export class ServiceA {
   apiService: ApiService;

   public constructor(_apiService: ApiService) {
      apiService = _apiService;
   }

   public async DoWork() {
      // Do some stuff
      const output: number = await apiService.RetrieveA(param1, param2);
      // Do something with the output
   }
}

这是修改后的 ApiService,它返回输出的 Promise 并限制实际的函数执行:

export class ApiService {
   // We keep the functions that need to be executed in this queue
   // and process them sequentially
   queue: (() => void)[] = [];

   public async RetrieveA(param1, param2): Promise<number> {
      // This resolver serves two purposes - it will be called when the
      // function is executed (to set the output), but will also be part
      // of the Promise that will be returned to the caller (so that the
      // caller can await for the result).
      let resolver: (value: number) => void;

      // This function will be executed by the RunQueue method when its
      // turn has come. It makes a call to the external API and when that
      // call succeeds - the resolver is called to return the result through
      // the Promise.
      const func = async () => {
         return this.CallApi(param1, param2).then(resolver);
      };

      this.queue.push(func);

      // This is the promise that we return to the caller, so that he
      // can await for the result. 
      const promise = new Promise<number>((resolve, reject) => {
         resolver = resolve;
      });

      return promise;
   }

   public async Run() {
      this.RunQueue(this.queue);
   }

   private async RunQueue(funcQueue: (() => void)[]) {
      // Get the first element of the queue
      const func = funcQueue.shift();

      // If the queue is empty - this method will continue to run
      // until a new element is added to the queue
      if (func) {
         await func();
      }

      // Recursively call the function again after 1 second
      // This will process the next element in the queue
      setTimeout(() => {
         this.RunQueue(funcQueue);
      }, 1000);
   }

   private async CallApi(param1, param2): Promise<number> {
      // Call the external API, process its output and return
   }
}

我希望代码中的 cmets 清楚说明我想要实现什么(以及如何实现)。

【讨论】:

  • 我觉得我到处评论这个,但我更喜欢const delay = time =&gt; new Promise(res=&gt;setTimeout(res,time)) 然后使用await delay(1000) :)
  • 这看起来比我的更好:) 感谢您的建议,我将在我的代码中使用它。还在学习:)
  • 哈,你看起来做得很好。我喜欢这种布局。 :) (你来自什么语言?)
  • 10 年的 C# 开发经验。是时候转向更现代的技术了。仍然很难适应,但学习新事物总是很有趣。
猜你喜欢
  • 1970-01-01
  • 2022-01-12
  • 1970-01-01
  • 2017-11-25
  • 1970-01-01
  • 2019-07-16
  • 2021-12-08
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多