【问题标题】:Why doesn't Node.js exit after Promise.race is finished?为什么 Promise.race 完成后 Node.js 不退出?
【发布时间】:2021-04-14 16:44:56
【问题描述】:

在 Node.js 中,我使用 Promise.race 来超时并取消 Request-Promise 库发出的请求。我的Promise.race 实现似乎阻止了该程序。

Promise.race 确实解析并返回,但之后,程序永远不会退出。

再次强调,等待确实完成并记录响应,但程序永远不会退出。

const rp = require('request-promise');

/**
 * @param {rp.RequestPromise} request 
 * @param {Number} ms 
 * @returns {Error}
 */
const delay = (request, ms) => new Promise((resolve, reject) => setTimeout(() => {
    request.cancel();
    return reject(new Error("Timeout"))
}, ms))

async function main() {
    try {
        const options = {
            method: 'GET',
            url: "https://api.ipify.org/?format=json",
        };

        const request = rp(options);
        const response = await Promise.race([request, delay(request, 500000)]);

        console.log(response)

    } catch (e) {
        console.log(e)
    }
}

main()

有人知道这是什么原因吗?

【问题讨论】:

  • @FZs 这里的重点是等待完成但程序永远不会退出。它甚至记录响应。我知道 await 做什么。
  • 问题不清楚。从不退出?我什至没有看到main 被调用。我认为你被投票是因为Promise.race 并不为人所知。谢谢你。
  • @StackSlave 我忘记复制那行,编辑问题以反映 main 被调用。
  • @StackSlave 主函数永远不会退出。我看到响应被记录下来。我认为setTimeout 阻止了函数退出。
  • Promise.race 只返回第一个 Promise 已解决或被拒绝。我没有看到任何说明其他 Promise 无法解决的文档,所以我认为 request.cancel() 正在发生,无论哪种方式。

标签: javascript node.js


【解决方案1】:

Promise.race()await 都不是问题的根源。是setTimeout

实际上,您的代码确实有效。一件小事,完成后它一直挂着。不是永远,只有在你的计时器到期之前。大约 500 秒。

Node.js 通过跟踪程序的所有可能输入来工作,并且仅在没有任何输入时退出(或者,当然,如果通过process.exit() 以编程方式退出或通过向其发送中断或终止信号手动退出) .但是,您的 setTimeout 输入;尽管它的完成是无操作的,但 Node.js 并不知道这一点,因此它使进程保持活动状态。

要解决此问题,您可以告诉 Node.js,setTimeout 并不重要,并且您的程序可能会通过 unreffing 退出,而不管它:

const rp = require('request-promise');

/**
 * @param {rp.RequestPromise} request 
 * @param {Number} ms 
 * @returns {Promise<Error>}
 */
const delay = (request, ms) => new Promise((resolve, reject) => 
    setTimeout(() => {
        request.cancel();
        return reject(new Error("Timeout"))
    }, ms)
    //Call unref on the interval ID object that setTimeout returned
    //Note that this is Node.js only.
    .unref()
)

async function main() {
    try {
        const options = {
            method: 'GET',
            url: "https://api.ipify.org/?format=json",
        };

        const request = rp(options);
        const response = await Promise.race([request, delay(request, 500000)]);

        console.log(response)

    } catch (e) {
        console.log(e)
    }
}

main()

这样,进程只会在请求完成之前保持活动状态;如果计时器触发得更快,它将取消请求并因此退出程序,但如果您的请求首先完成,它可能会直接退出而不等待计时器。

但是,请注意,此类 setTimeout 调用会累积(如果事件循环中有其他东西使进程保持活动状态,则此代码不会取消超时),这会对性能产生负面影响.

因此,如果您的代码经常执行此操作(或者,在您的情况下,超时很长),那么您应该找到一种方法来在您的请求完成时以编程方式取消超时。例如:

const rp = require('request-promise');

/**
 * @param {rp.RequestPromise} request 
 * @param {Number} ms 
 * @returns {Promise<Error>}
 */
const delay = (request, ms) =>
  new Promise((resolve, reject) => {
    const id = setTimeout(() => {
      request.cancel();
      return reject(new Error("Timeout"))
    }, ms)
    
    request
      //This will handle the promise (and makes possible unhandled-rejection warnings away) to avoid breaking on errors, but you should still handle this promise!
      .catch(() => {})
      .then(() => clearTimeout(id))
  }
)

async function main() {
  try {
    const options = {
      method: 'GET',
      url: "https://api.ipify.org/?format=json",
    };

    const request = rp(options);
    const response = await Promise.race([request, delay(request, 500000)]);

    console.log(response)

  } catch (e) {
    console.log(e)
  }
}

【讨论】:

  • @StackSlave 我要提一下,但我忘了。感谢您提醒我,我编辑了我的帖子。
  • 你为什么要删除克隆的东西?
  • @jeffbRTC 因为我意识到我错了。调用.then() 确实克隆了promise,但也处理了它,这是我试图避免的(我试图避免将promise 标记为'handled',因为虽然这段代码确实调用了.catch(),但没有' t 真正处理它,只是丢弃错误;所以如果你的代码没有在其他地方处理它,它应该发出警告,但这样它不会,因此我在那里添加了评论)
【解决方案2】:

这是我的两分钱:

const rp = require('request-promise');
function Canceller(){
  let interval;
  this.cancellable = (request, milliseconds)=>{
    return new Promise((resolve, reject)=>{
      interval = setTimeout(()=>{
        request.cancel(); reject(new Error('timeout'));
      }, milliseconds);
    });
  }
  this.stopCancel = ()=>{
    clearTimeout(interval); interval = undefined;
    return this;
  }
} 
async function main(){
  try{
    const options = {
      method: 'GET',
      url: 'https://api.ipify.org/?format=json'
    }
    const request = rp(options), canceller = new Canceller;
    const response = await Promise.race([request, canceller.cancellable(request, 500000)]);
    if(response === request)canceller.stopCancel();
    console.log(response)
  }catch(e){
    console.log(e)
  }
}
main();

【讨论】:

    【解决方案3】:

    我对其他答案做了一个抽象,

    const withTimeout = (millis, promise) => {
        const request = promise;
        
        const timeout = new Promise((resolve, reject) => {
            let id = setTimeout(
                () => reject(promise.cancel()),
                millis)
    
            promise
                .then() //Clone the promise (prevents 'handling' the original one)
                .catch(() => { }) //Disregard rejections on this chain
                .then(() => clearTimeout(id))
        })
    
        return Promise.race([
            request.json(),
            timeout
        ]);
    };
    

    【讨论】:

    • 不确定您所说的“防止'处理'原始文件”是什么意思,但.then() 通常对承诺毫无意义。尽管您的 promise 参数看起来不像标准的 Promise,但您在其上调用了 .cancel().json() 方法。
    • @Bergi FZ 发表的评论。看看最上面的答案。
    • 啊,但是he already corrected it
    【解决方案4】:

    通过在原生之上构建的自定义 Promise,它可以开箱即用:

    const {CPromise} = require('c-promise2');
    const rp = require('request-promise');
    
    async function main() {
      try {
        const options = {
          method: 'GET',
          // request with 2sec delay
          url: "https://run.mocky.io/v3/7b038025-fc5f-4564-90eb-4373f0721822?mocky-delay=2s"
        };
    
        const response = await CPromise.from(rp(options)).timeout(1000);
    
        console.log(response)
    
      } catch (e) {
        console.log(e) // CanceledError: timeout
      }
    }
    
    main()
    

    或者根据你的代码:

    const {CPromise} = require('c-promise2');
    const rp = require('request-promise');
    
    async function main() {
      try {
        const options = {
          method: 'GET',
          // request with 2sec delay
          url: "https://run.mocky.io/v3/7b038025-fc5f-4564-90eb-4373f0721822?mocky-delay=2s"
        };
    
        const request = rp(options);
        const response = await CPromise.race([request, CPromise.delay(10000)]);
    
        console.log(response)
    
      } catch (e) {
        console.log(e)
      }
    }
    
    main()
    

    它会在需要时自动取消网络请求和/或清除计时器,以便节点进程在其中一个承诺完成后立即退出。

    【讨论】:

      猜你喜欢
      • 2012-10-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-05-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-01-25
      相关资源
      最近更新 更多