如何通过 API 调用停止一个递归的 setTimeout?

4
我有一个NextJS应用程序,在服务器启动时运行一个递归的setTimeout。我需要创建一个API端点,可以启动和停止这个循环(在生产环境中更好地控制它)。该循环用于处理从另一个API端点添加的数据库中的项目。
  import { clearTimeout } from "timers";

  var loopFlag = true;

  export function loopFlagSwitch(flag: boolean) {
    loopFlag = flag;
  }

  export async function loop() {
    
    try {
      // Retrieve all unprocessed transactions
      const unprocessedTransactions = await prisma.transaction.findMany({
        take: 100,
        where: { status: "UNPROCESSED" },
      });

      // Loop through transactions and do stuff
      for (const transaction of unprocessedTransactions) {
        //stuff
      }
    } catch (e) {
      // handle error
    }

    if (loopFlag === true) { 
      setTimeout(loop, 1000);  //if flag changes, this will stop running
    }
  }

  if (require.main === module) {
    loop(); // This is called when server starts, but not when file is imported
  }


我使用setTimeout而不是setInterval的原因是在处理从数据库检索得到的项目时可能会发生许多错误。然而,通过等待几毫秒可以解决这些错误。因此,以下模式的好处是如果发生错误,循环立即重新开始,并且错误不会出现,因为经过了一毫秒(这是由于并发问题造成的——暂时忽略这个问题)。
我有一个端点来尝试启动和停止这个循环,只需调用loopFlagSwitch函数。
import { NextApiRequest, NextApiResponse } from "next";
import { loopFlagSwitch } from "services/loop";

async function handler(req: NextApiRequest, res: NextApiResponse) {
  try {
    loopFlagSwitch(req.body.flag);
  } catch (error) {
    logger.info({ error: error });
  }
}

export default handler;


问题是,即使调用了这个端点,setTimeout循环仍然在继续。为什么它没有注意到标志的变化呢?

Node会跟踪函数作用域和声明的变量,这意味着示例应该可以工作。您可以通过一个最小的示例轻松验证这一点。您是否检查过req.body是否为空对象或未定义等。可能是因为您没有使用正确的body解析器或者没有在API中发送正确的“Content-Type”头部,导致req.body.flag始终返回未定义。 - ibrahim tanyalcin
4个回答

3

clearTimeout()

全局方法clearTimeout()通过调用setTimeout()来清除先前建立的超时。

要清除超时,请使用setTimeout()返回的ID:

用法

const myTimeout = setTimeout(function, milliseconds);
//Then you can to stop the execution by calling clearTimeout():

clearTimeout(myTimeout);

循环标志作为条件

...
if (loopFlag === true) { 
    myTimeout();
} else {
    clearTimeout(myTimeout)
}
...

添加 abortTimer 函数

完整代码



export function loopFlagSwitch(flag) {
  flag === true ? loop : abortTimer()
}

// set timeout
var myTimeout = setTimeout(loop, 1000);


function abortTimer() { // to be called when you want to stop the timer
  clearTimeout(myTimeout);
}

export async function loop() {
    try {
      // Retrieve all unprocessed transactions
      let d = "Retrieve all unprocessed transactions"
      process.stdout.write(d + '\n');

      // Loop through transactions and do stuff
      for (let i = 0; i<10;  i++) {
        //stuff
        let c = "second loop"
        process.stdout.write(c + '\n');
      }
    } catch (e) {
      // handle error
      console.log("error ", e)
    }  finally {
      myTimeout = setTimeout(loop, 1000); // repeat myself 
    }
    
  }

  if (require.main === module) {
    loop(); // This is called when server starts, but not when file is imported
  }

我已经尝试过这个方法,但似乎并没有起作用。当我调用API端点将标志更改为false时,递归循环中的变量似乎没有发生变化。我的问题不在于清除setTimeout,而是通过API调用更改变量loopFlag。我不确定是否可以在递归循环开始后更改变量。 - Dimitri Borgers
switch函数对循环函数没有影响,在您的情况下,我认为最好将循环函数包装在switch函数内部,这样您就可以传递标志参数。 - Jamal Eddine Naamani
我编辑了我的答案,展示了解决这个问题的一种方式 @DimitriBorgers - Jamal Eddine Naamani

2

这个标志不起作用是因为节点不维护文件的状态,导入只关心从文件中获取的内容,它不关心其中声明的变量的状态。

即使clearTimeout()函数可能足够,我认为你可以使用更好的选项来停止这个循环。

使用JS类!

不要仅仅使用没有状态的函数。你可以实例化一个在服务器上运行的类,并调用一个名为"shouldKeepLooping"的内部布尔值:


class NextJsLooper { // or whatever better name you can use

  private shouldKeepLooping: boolean

  constructor () {
    this.shouldKeepLooping = true
  }
  
  public shouldKeepLooping(value) { this.shouldKeepLooping = value }

  public async loop () {
    if (shouldKeepLooping) {
      //... rest of the loop code
      setTimeout(this.loop, 1000);
    }
  }
}

这样,如果您将该值设置为false,它将自动停止。因为这是对对象的引用。
请记住,您需要将此实例保持活动状态,可能作为全局变量,并且需要使其可以被nextJS访问。
您可以使用Symbol.for和Node全局变量,在服务器上维护实例保存在某个地方!

2
这是一个优雅的解决方案。但我认为,通过使用类来管理内存状态 + 使用 Jamal 的答案中提到的 clearTimeout 的组合是正确的选择。 clearTimeout 应该效果更好,并且维护类将允许更好的未来重用,例如添加更多方法等。 - brianangulo
同意,我认为清除超时可能并不必要,因为在最后一次循环调用中递归将停止,不会再发生更多的超时,对吧?但也许同时使用两者是正确的方式。 - Lautaro urtiaga

0

我的建议是引入信号的使用。

Pusher这样的服务将触发事件,由交易处理器进行监听。

您的交易处理API / 以上代码或任何其他代码

信号动作如“开始”和“停止”将随时被触发,即使在生产中也可以通过前端或通过将用于将循环标志更改为true或false的推送器门户。


0

你可以检索负责操作的线程,并在给定的预期时间后中断,如果你的操作花费的时间超过了必要的时间,可以记录一些信息来通知你超时。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接