Node.js Serverless 中的异步任务:为什么必须返回并等待 Promise

2021年07月04日137 次阅读0 人喜欢
技术Node.jsserveless
所属合集

Node.js Serverless 中的异步任务:为什么必须返回并等待 Promise

本文讨论一个常见问题:路由处理函数调用了异步任务,但任务没有完成就已经返回响应。在 Serverless 环境中,响应返回后运行时可能被冻结或回收,因此“已经放进定时器”不等于“任务一定会执行完成”。

一、问题示例

下面的代码虽然把路由函数声明成了 async,但 track() 没有返回 Promise:

javascript 复制代码 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-chevron-left md-editor-icon"><circle cx="12" cy="12" r="10"/><path d="m14 16-4-4 4-4"/></svg>
router.get('/track', async (req, res) => {
  track(req.query);
  res.type('image/png').send('1');
});

function track(query) {
  setTimeout(() => {
    // 执行统计或其他异步任务
  }, 2000);
}

这里有两个问题:

  1. track() 返回的是 undefined,路由无法等待它。
  2. setTimeout() 只是把回调放入未来的任务队列,并不代表回调已经完成。

在普通的常驻 Node.js 进程中,定时器通常还有机会执行;但在 Serverless 中,平台可能在响应发送后暂停或回收当前实例,后台任务就可能丢失。

另外,JavaScript 的函数名是 setTimeout,不是 setTimeOut。大小写错误会直接导致 ReferenceError

二、让异步任务返回 Promise

如果这个任务必须在响应前完成,可以把它封装成 Promise,并在路由中使用 await

javascript 复制代码 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-chevron-left md-editor-icon"><circle cx="12" cy="12" r="10"/><path d="m14 16-4-4 4-4"/></svg>
function track(query) {
  return new Promise((resolve, reject) => {
    setTimeout(async () => {
      try {
        // 这里执行真正的异步操作
        // await saveTrackRecord(query);
        resolve();
      } catch (error) {
        reject(error);
      }
    }, 2000);
  });
}

router.get('/track', async (req, res, next) => {
  try {
    await track(req.query);
    res.type('image/png').send('1');
  } catch (error) {
    next(error);
  }
});

这样调用关系就完整了:

text 复制代码 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-chevron-left md-editor-icon"><circle cx="12" cy="12" r="10"/><path d="m14 16-4-4 4-4"/></svg>
HTTP 请求
  -> 路由处理函数
  -> await track()
  -> Promise 完成
  -> 返回 HTTP 响应

三、如果底层 API 本身已经返回 Promise

不需要再手动套一层 Promise。直接返回或等待原始 Promise 即可:

javascript 复制代码 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-chevron-left md-editor-icon"><circle cx="12" cy="12" r="10"/><path d="m14 16-4-4 4-4"/></svg>
async function track(query) {
  await saveTrackRecord(query);
}

router.get('/track', async (req, res, next) => {
  try {
    await track(req.query);
    res.type('image/png').send('1');
  } catch (error) {
    next(error);
  }
});

手动创建 Promise 时,最容易出现的问题是忘记调用 resolve()reject(),导致请求一直等待,最终触发平台超时。

四、什么时候不应该阻塞响应

如果 track() 只是访问统计、日志记录或消息上报,通常不应该为了它让用户等待两秒。更合理的方案是:

  • 把事件写入消息队列。
  • 写入可靠的数据库或缓存队列。
  • 使用云函数平台提供的异步任务机制。
  • 让消费者进程负责重试和失败记录。

例如,路由只负责把事件写入队列:

javascript 复制代码 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-chevron-left md-editor-icon"><circle cx="12" cy="12" r="10"/><path d="m14 16-4-4 4-4"/></svg>
router.get('/track', async (req, res, next) => {
  try {
    await eventQueue.publish({
      type: 'track',
      payload: req.query,
    });
    res.type('image/png').send('1');
  } catch (error) {
    next(error);
  }
});

这比在请求中固定等待两秒更适合 Serverless。关键不是“必须 await 所有任务”,而是要明确任务的可靠性要求:必须完成的任务要等待,可延迟的任务要交给可靠队列。

五、测试异步行为

可以用一个 Promise 测试函数确认任务确实在响应前完成:

javascript 复制代码 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-chevron-left md-editor-icon"><circle cx="12" cy="12" r="10"/><path d="m14 16-4-4 4-4"/></svg>
async function demo() {
  const start = Date.now();
  await track({ id: 1 });
  console.log(`耗时:${Date.now() - start}ms`);
}

demo();

如果 track() 没有返回 Promise,demo() 会几乎立即结束;如果返回了 Promise,耗时会接近定时器设置的时间。

总结

  • async 不会自动等待函数内部启动的所有任务。
  • 需要等待的异步操作必须返回 Promise,并由调用方 await
  • setTimeout 只是延迟执行,不提供任务持久化能力。
  • Serverless 中不要依赖响应返回后的“后台继续执行”。
  • 统计、日志等非关键任务应优先使用队列或平台异步机制。

原文中的主要问题是 track() 没有返回 Promise、setTimeout 拼写错误,以及没有区分“必须完成”和“可以异步”的任务。

加载评论中...