【问题标题】:Long-running asynchronous file copies block browser requests长时间运行的异步文件副本阻止浏览器请求
【发布时间】:2022-10-19 00:18:27
【问题描述】:

Express.js 为 Remix 应用程序提供服务。服务器端代码在启动时设置了几个计时器,这些计时器每隔一段时间就会执行各种后台作业,其中一个会检查远程 Jenkins 构建是否完成。如果是这样,它会将多个大型 PDF 从一个网络路径复制到另一个网络路径(都在 GSA 上)。

一个函数创建了一系列链式 glob+copyFile 承诺:

  import { copyFile } from 'node:fs/promises';
  import { promisify } from "util";
  import glob from "glob";
...
  async function getFiles() {
            let result: Promise<void>[] = [];
            let globPromise = promisify(glob);
            for (let wildcard of wildcards) { // lots of file wildcards here
              result.push(globPromise(wildcard).then(
                (files: string[]) => {
                    if (files.length < 1) {
                        // do error stuff
                    } else {
                        for (let srcFile of files) {
                            let tgtFile = tgtDir + basename(srcFile);
                            return copyFile(srcFile, tgtFile);
                        }
                    }
                },
                (reason: any) => {
                    // do error stuff
                }));
            }
            return result;
 }

另一个异步函数获取该数组并对其执行 Promise.allSettled :

copyPromises = await getFiles();
console.log("CALLING ALLSETTLED.THEN()...");
return Promise.allSettled(copyPromises).then(
    (results) => {
        console.log("ALLSETTLED COMPLETE...");

在“CALLING”和“COMPLETE”消息之间,可能需要几分钟的时间,服务器不再响应浏览器请求,超时。

但是,在此期间,仍然可以看到我的其他活动后端计时器在服务器控制台日志中运行并正常完成(出于测试目的,我每 5 秒运行一次,并且在这些文件副本爬行时它一遍又一遍地运行非常平稳沿着)。

所以它并没有阻止整个服务器,它似乎只是阻止浏览器请求被处理。并且一旦在日志中弹出“COMPLETE”消息,浏览器请求就会再次正常处理。

Express 启动脚本基本上只是为 Remix 执行此操作:

const { createRequestHandler } = require("@remix-run/express");
...
app.all(
    "*",
        createRequestHandler({
            build: require(BUILD_DIR),
            mode: process.env.NODE_ENV,
        })
);

这是怎么回事,我该如何解决?

【问题讨论】:

  • 我会使用child-process 在另一个线程中运行任务
  • 哇,好诡异! fs.copyFile(srcFile, tgtFile) 将服务器连接到 HTTP 请求,但使用 child_process.exec("copy " + srcFile + " " + tgtFile) 根本不会。浏览器请求在浏览所有这些副本时会立即处理!后者依赖于操作系统,但我当然可以接受,因为它处理问题的方式非常简单(而且很好)。我仍然不明白的是……鉴于据报道 Node“非常擅长异步 I/O”,为什么 async copyFile 会有效地阻塞服务器?
  • 裸体在一个线程中运行。它适用于多个短期任务。如果某些操作需要很长时间,它会阻塞。
  • 我不知道混音,createRequestHandler 是做什么的?它是否尝试从文件系统提供文件?
  • "它复制了几个大的 PDF“ - 我们在这里谈论多少个文件?

标签: javascript express remix.run


【解决方案1】:

显然没有进一步的讨论,而且我还没有确定为什么异步 I/O 功能会阻止服务器响应,所以我将继续发布一个答案,基本上是来自 cmets 的 Konrad Linkowski 的解决方案:使用操作系统来执行副本而不是使用 copyFile()。它可以归结为代替 getFiles 中的 glob+copyFile 调用:

const exec = util.promisify(require('node:child_process').exec);
...
async function getFiles() {
   ...
   result.push( exec("copy /y " + wildcard + " " + tgtDir) );
   ...
}

这并没有表现出任何破坏请求的行为;在整个复制过程中(很多分钟),浏览器请求会立即得到处理。

这是一个特定于操作系统的解决方案,因此不可移植,但在我们的情况下这很好,因为我们可能会在未来很多年为这个应用程序使用 Windows 服务器。当然,如果需要,运行时操作系统检测可用于使命令在其他操作系统上运行。

【讨论】:

    最近更新 更多