【问题标题】:use async operation inside cycle for with timeout在循环内使用异步操作超时
【发布时间】:2021-05-31 19:10:27
【问题描述】:

问题:我有一个要删除的用户列表。不仅用户本人被删除,而且与他相关的大量数据也被删除。一些涉及删除用户信息的表包含数百万条记录。 所有内部进程在删除时都是异步启动的。如果删除一个用户时没有问题,并且后台所有进程都成功(即客户端不期望响应),那么如果您运行其中几个,这会占用所有服务器资源,这只要网站被移除,自然会对网站的工作产生负面影响。

for (let i = 0; i < users.length; i++) {
    const user = users[i];
    await this.removeInstance(user, removedBy);
}

this.removeInstance 包含许多其他异步操作。

我想要的:每隔 3 分钟启动一个异步操作 this.removeInstance。 数据删除的速度对我来说并不重要,但根据我的观察,1-2 分钟就足以“完全完成”一个用户。

注意: this.removeInstance 返回已删除的用户,但我不能使用它,因为已经有十几个进程在后台运行以清除他的信息。

感谢您的帮助!

【问题讨论】:

    标签: node.js mongodb express async-await


    【解决方案1】:

    您可以创建一个将异步任务完成延迟到最低限度的包装函数:

    const wait = ms =>
      new Promise(resolve => setTimeout(resolve, ms));
    
    const delayCompletion = minTime => task =>
      Promise.allSettled([task(), wait(minTime)])
        .then(([result]) => result.status === "fulfilled"  
          ? result.value 
          : Promise.reject(result.reason)
        );
    

    例子:

    const wait = ms =>
      new Promise(resolve => setTimeout(resolve, ms));
    
    const delayCompletion = minTime => task =>
      Promise.allSettled([task(), wait(minTime)])
        .then(([result]) => result.status === "fulfilled"  
          ? result.value 
          : Promise.reject(result.reason)
        );
          
    
    //showcase:
    
    async function main() {
      console.log("-- start loop --");
      for(let i = 0; i < 3; i++) {
        console.log(`start iteration ${i}`);
        
        const delayAtLeastTwoSeconds = delayCompletion(2000);
        const oneSecondtask = () => wait(1000);
        
        await delayAtLeastTwoSeconds(oneSecondtask);
        
        console.log(`finish iteration ${i}`);
      }
      console.log("-- finish loop --");
    }
    
    main();

    Promise.allSettled() 一直等到所有的 Promise 都不再挂起。我们给它一个异步任务,它添加另一个以最小延迟,所以如果task 提前完成,那么它仍然必须等到延迟计时器完成。相反,如果延迟计时器首先结束,但task 尚未结束,则仍会等待。

    Promise.allSettled 解析为一组承诺,其中每个承诺的状态为"fulfilled""rejected"。我们只对数组的第一项感兴趣,因为它是task - 第二项是延迟。如果成功 以原始拒绝原因拒绝,我们只返回它的值,如果不是。这样delayCompletion 仍然保留与原始承诺相同的语义,并且任何使用原始承诺的代码都可以透明地使用来自delayCompletion 的承诺。

    在某些方面,这与 Promise.race() 相反,它解析为首先解析的承诺。相反,我们等待最后一个,但我们确实有一个固定的返回值。

    使用这个辅助函数,您的代码可以这样转换:

    const safeDelay = delayCompletion(3 * 60 * 1000); //3 minutes
    
    for (let i = 0; i < users.length; i++) {
        const user = users[i];
        await safeDelay(() => this.removeInstance(user, removedBy));
    }
    

    【讨论】:

    • 有趣但... UnhandledPromiseRejectionWarning: TypeError: Promise.allSettled 不是一个函数。节点版本?
    • @RomanKovalevsky according to MDN,这将适用于 Node 12.9.0 及更高版本。编辑:似乎there is an npm package 可以用于旧环境。
    • 谢谢。我的版本“@types/node”:“^10.7.1”。我认为您的解决方案是最好的,但不幸的是我无法应用它。而且我还没有准备好升级 Node :)
    • @RomanKovalevsky 我已经编辑了我的评论。如果Promise.allSettled 不可用,您可以导入一个包以使用allSettled
    • 是的,它确实有效。据我了解,我现在可以设置 1 分钟的延迟,如果突然操作及其所有子操作都没有在分配的时间内完成,我不用担心?
    【解决方案2】:

    您好,您可以伪造一个睡眠功能并使用它

    function sleep(time = 0){
      return new Promise(resolve=>{
        setTimeout(()=>{
          resolve()
        },time)
      })
    }
    
    for (let i = 0; i < users.length; i++) {
        const user = users[i];
        await this.removeInstance(user, removedBy);
        await sleep(1000*60*3)
    }

    【讨论】:

    • 谢谢,我在下面发布了这个答案,我在链接中找到了这个答案。感谢您的关注和您的时间,我注意到您的回答也有效。祝你有美好的一天!
    【解决方案3】:

    很抱歉打扰您,但已找到解决方案。 How do I add a delay in a JavaScript loop?

    决赛:

    public async removeBatchUsers(usersEmailsList: string[], removedBy: IIdentity) {
        const users: UserSchema[] = await User.find({userEmail: {$in: usersEmailsList}});
    
        const timer = ms => new Promise(res => setTimeout(res, ms));
    
        async function load (that) {
            for (let i = 0; i < users.length; i++) {
                const user = users[i];
                await that.removeInstance(user, removedBy);
                await timer(10000);
            }
        }
    
        load(this);
    }
    

    上面使用 Promise.allSettled 的解决方案是最正确和最安全的。经过测试,在我的情况下,我必须另外安装“promise.allsettled”库(23Kb)

    【讨论】:

    • 当然,定时器应该设置的少一些,只是要确保它们不会同时执行
    【解决方案4】:

    如果您不太担心整个过程需要多长时间,您可以使用带有 await 的 for..of 循环在数组上串联运行 this.removeInstance

    for (let user of users) {
      await this.removeInstance(user)
    }
    

    这将在开始下一个用户之前等待每个用户被删除 - 然后您可以向客户端返回 http 202 以指示消息已收到并正在处理,也许还实现轮询端点以检查状态的工作。

    【讨论】:

    • OP 担心需要多长时间。或者更确切地说,它需要多并希望在迭代之间施加最小延迟。
    • 这会启动很多嵌套的异步操作并发运行,我写过这个问题
    • 也许我误解了这个问题 - 我认为“数据删除的速度对我来说并不重要”意味着如果它没有吃掉服务器的所有资源,该过程可能需要一段时间(即启动数百万个并发 javascript 操作)。我认为这里有一个关于构建您的应用程序的更一般的观点 - 如果大量后台工作正在损害您的网络服务器的性能,您可能希望将其拆分为一个可以独立扩展的单独服务。
    • 是的,以后我也是这样打算的,因为删除用户时伴随的所有操作都是明显分离独立的,因此可以单独处理它们的执行。
    猜你喜欢
    • 2013-06-09
    • 1970-01-01
    • 2018-10-17
    • 1970-01-01
    • 2017-11-03
    • 1970-01-01
    • 1970-01-01
    • 2020-12-26
    • 2014-04-10
    相关资源
    最近更新 更多