【问题标题】:How can I prevent the losing arm of a Promise.race from activating?如何防止 Promise.race 失去的手臂被激活?
【发布时间】:2022-01-18 07:46:06
【问题描述】:

我将相同的查询发送到大量(好吧,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;
}

但是,我现在目睹了 Promise 的 两个 分支运行到完成,即 out 数组包含(一些)集群报告数据和 allnone 我的集群报告超时:

  • 如果没有超时,Promise.all 不会等待setTimeouts,但节点进程会(即如果我将超时设置为 60 秒,那么节点进程将仅在 60 秒后退出)
  • 如果达到任何超时,Promise.all 将等待超时发生...这意味着 所有 setTimeouts 最终触发。

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

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

【问题讨论】:

  • 也许我所要做的就是使out.push({cluster: cluster, error: 'timed out'}) 条件为out 尚未包含来自cluster 的数据?
  • 也许AbortController 可以帮助您摆脱困境?我不确定getAllPods 在幕后做什么,但如果它在做网络请求之类的事情,请检查您的网络请求库是否支持AbortSignal,并在setTimeout 中调用abortController.abort() - 这可能会停止你的另一个 Promise.race 等待等待

标签: javascript typescript promise cancellation


【解决方案1】:

是的,你有两个任务正在运行,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

【讨论】:

    【解决方案2】:

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

    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,这发生在我的程序生成其输出几秒钟后;我首先这样做的部分原因是存在故障模式,其中进程只是挂起等待服务器响应被防火墙吃掉的请求。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-07-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-08-17
      • 2013-04-08
      相关资源
      最近更新 更多