如何防止 Promise.race 失去的手臂激活?

How can I prevent the losing arm of a Promise.race from activating?

我正在向大量(好吧,n=5)相同的端点(一些 kubernetes 集群)发送相同的查询并将结果整理在一起。我希望请求并行发出并在一段时间后超时;我希望在不妨碍进一步进展的情况下向用户报告失败。

作为所需输出的示例:

$ node find_broken_pods.js
tomato: error: timed out
Cluster  Pod         Reason
potato   bar-foos    Invalid PVC
potato   foo-eggnog  Invalid image
yoghurt  spam-baz    Invalid name

$ node find_broken_pods.js
Cluster  Pod         Reason
potato   bar-foos    Invalid PVC
potato   foo-eggnog  Invalid image
yoghurt  spam-baz    Invalid name
tomato   what-bat    Insufficient jQuery

第一次,无论出于何种原因,我们未能从番茄集群中获得答案,但我们能够从其他集群中枚举资源。第二次,没有超时,我们能够列出所有内容。

我以前想过这个:

export async function queryAll(): Promise<{cluster: string; deployments: V1Pod[]}[]> {
  const out: {cluster: string; result: V1Pod[]}[] = [];

  const promises: Promise<number>[] = [];
  for (const cluster of Object.values(CLUSTERS)) {
    promises.push(
      Promise.race([
        new Promise<number>((_, reject) => setTimeout(() => reject(new Error(`${cluster}: timed out`)), 5000)),
        new Promise<number>((resolve, _) =>
          getAllPods(cluster)
            .then(pods => resolve(out.push({cluster: cluster, result: pods})))
        ),
      ])
    );
  }

  await Promise.all(promises);

  return out;
}

运行 并行执行所有操作,但单个故障会导致整个功能崩溃和燃烧。我想我可以把它改成这样:

export async function queryAll(): Promise<{cluster: string; deployments?: V1Deployment[]; error?: string}[]> {
  const out: {cluster: string; result?: V1Pod[]; error?: string}[] = [];

  const promises: Promise<number>[] = [];
  for (const cluster of Object.values(CLUSTERS)) {
    promises.push(
      Promise.race([
        new Promise<number>((resolve, _) =>
          setTimeout(() => {
            resolve(out.push({cluster: cluster, error: 'timed out'}));
          }, 5000)
        ),
        new Promise<number>((resolve, _) =>
          getAllPods(cluster)
            .then(pods => resolve(out.push({cluster: cluster, result: pods})))
        ),
      ])
    );
  }

  await Promise.all(promises);

  return out;
}

但是,我现在正在见证 承诺 运行 完成,即 out 数组包含(一些)集群报告数据allnone 的集群报告超时:

我反而期望 Promise.race 失去的手臂会以某种方式被杀死或被阻止 运行ning。

有些事情告诉我,我的方法从根本上是错误的...我怎样才能提高我的容错能力?

看起来这已经足够满足我的需求了,但还是不行:

export async function queryAll(): Promise<{cluster: string; deployments: V1Pod[]}[]> {
  const out: {cluster: string; result: V1Pod[]}[] = [];

  const promises: Promise<number>[] = [];
  for (const cluster of Object.values(CLUSTERS)) {
    promises.push(
      Promise.race([
        new Promise<number>((_, reject) => setTimeout(() => reject(new Error(`${cluster}: timed out`)), 5000)),
        new Promise<number>((resolve, _) =>
          getAllPods(cluster)
            .then(pods => resolve(out.push({cluster: cluster, result: pods})),
                  error => {
                    process.stderr.write(`${cluster}: ${error}\n`);
                    resolve(0);
              })
        ),
      ]).catch(error => {
        process.stderr.write(`${cluster}: ${error}\n`);
        return 0;
      })
    );
  }

  await Promise.all(promises);

  return out;
}

由于重复的错误处理,这可能会导致此输出,我不太介意;例如,如果我用 /etc/hosts 强制失败,我得到:

$ node find_broken_pods.js
tomato: Error: timed out
Cluster  Pod         Reason
potato   bar-foos    Invalid PVC
potato   foo-eggnog  Invalid image
yoghurt  spam-baz    Invalid name
tomato: Error: connect ECONNREFUSED 0.0.0.0:443

我不太高兴的是,节点特别等待了足够长的时间让任何库抛出 ECONNREFUSED,这发生在我的程序完成生成其输出几秒钟之后;我首先这样做的部分原因是存在故障模式,在这种模式下,进程只是挂起等待服务器响应被防火墙吃掉的请求。

是的,您已经将任务 运行 和 pushing 对象都添加到了 out 数组。没有什么能阻止他们 - Promise.race 不会神奇地撤消为创建传递给它的承诺而采取的步骤。

无论是否已经报告超时或结果,您都可以通过为每个集群设置一个标志来解决这个问题,这样比赛的失败者就不会报告他们的状态。

但是,只使用承诺实现的结果值要简单得多,Promise.race 已经转发了:

interface QueryResult {cluster: string; deployments?: V1Deployment[]; error?: string}
export async function queryAll(): Promise<QueryResult[]> {
  const promises: Promise<QueryResult>[] = Object.values(CLUSTERS).map(cluster =>
    Promise.race([
      new Promise(resolve => {
        setTimeout(() => {
          resolve({cluster: cluster, error: 'timed out'});
        }, 5000)
      }),
      getAllPods(cluster).then(pods => ({cluster: cluster, result: pods}))
    ])
  );

  const out = await Promise.all(promises);

  return out;
}

使用这种方法,您甚至可以按照与 CLUSTERS 相同的顺序获得结果,而不是按照它们返回的顺序。如果您不想那样,您仍然可以使用 push 的原始方法 - 只需根据每场比赛的结果来做!

export async function queryAll(): Promise<QueryResult[]> {
  const out: QueryResult[] = [];
  await Promise.all(Object.values(CLUSTERS).map(cluster =>
    Promise.race([
      new Promise(resolve => {
        setTimeout(() => {
          resolve({cluster: cluster, error: 'timed out'});
        }, 5000)
      }),
      getAllPods(cluster).then(pods => ({cluster: cluster, result: pods}))
    ]).then(result => {
      out.push(result);
    })
  ));
  return out;
}

顺便说一句,还可以考虑 Promise.allSettled 而不是 Promise.all - 你也可以 reject 从你的超时承诺中得到。