使用服务工作者正确地以角度方式超时 HTTP 请求

Posted

技术标签:

【中文标题】使用服务工作者正确地以角度方式超时 HTTP 请求【英文标题】:Timeout an HTTTP request properly in angular with a service worker 【发布时间】:2022-01-21 09:31:39 【问题描述】:

作为我的团队正在添加的一项新功能的一部分,有人问我,当特定的 HTTP Post 请求失败时(当互联网速度很慢或没有可用的互联网时),我将在整个跨度周期内每 X 秒重试一次请求Y 秒。例如,在 8 秒的周期内每 1 秒。 这是我出来的代码:

    return this.carService.saveCar(car)
      .pipe(this.isRetryable() ? retryWhen(errors => 
        return errors.pipe(mergeMap(response => 
          if (response.status === TIMEOUT_EXCEPTION_CODE || response.status === UNKNOWN_EXCEPTION_CODE) 
            return of(response).pipe(delay(this.saveCarRetryTimeout)) // X seconds
          
          
          return throwError(response);
      ));
    ) : tap(),
    this.isRetryable() ? timeout(this.saveCarProccessTime) : tap(), // Y Seconds
    tap((carId: number) => 
      this.logger.info(saveCarBaseLog.setStatus(LogStatus.COMPLETE));
    ),
    catchError((err) => 
      this.logger.error(saveCarBaseLog).setStatus(LogStatus.FAILED)); 
      return throwError(err);
    ));

isRetryable() 函数只是检查我们是否同时设置了 X 和 Y 配置,因此它不会影响进程。

在这样做并看到它在本地和开发环境中都运行良好后,我们上传了版本。第二天我们遇到了一个问题——在 preprod 和 prod 环境中,一些汽车被保存了两次。 经过我的调查,这个问题似乎来自我们拥有的服务工作者 - 每当发生完全超时时,请求本身就会超时,尽管与之关联的 FETCH 请求永远不会被取消,这在互联网刚刚启动时会导致问题慢(FETCH 请求最终成功,我们没有得到任何关于它的迹象)。 我真的不知道该怎么做,所以欢迎任何帮助!

我不能上传网络的截图,因为它是一个私有网络, 但在 chrome 的网络部分,它看起来像这样:

POST 请求 - saveCar - XHR - 504 超时 POST 请求 - (ServiceWorker) saveCar - FETCH - 504 超时 POST 请求 - saveCar - XHR - 504 超时 POST 请求 - (ServiceWorker) saveCar - FETCH - 504 超时 POST 请求 - saveCar - XHR - 504 超时 POST 请求 - (ServiceWorker) saveCar - FETCH - 200 成功(有问题的那个)

【问题讨论】:

我无法对您的缩进做出正面或反面。很确定您对管道的调用没有右括号?此外,您的最终 catchError 结束时没有 return 语句。在那之后,你有一个悬空的return 和关闭的,它们可能都应该属于catchError。这会解析吗? @MrkSef 嘿,我添加了右括号并将代码格式化为 JS。对不起,这是我第一次发布这样的代码,所以我尽力了。感谢您指出这一点:) 现在看起来不错?? 【参考方案1】:

问题

稍微简化后,我是这样理解你目前实现的逻辑的:

class arbitraryClass
  
  /* ... arbitrary code ... */

  arbitraryMethod()
    return this.carService.saveCar(car).pipe(
      this.retryTimeoutLogic(),
      tap(
        complete: () => this.logger.info(saveCarBaseLog.setStatus(LogStatus.COMPLETE)),
        error: err => this.logger.error(saveCarBaseLog.setStatus(LogStatus.FAILED))
      )
    );
  

  retryTimeoutLogic<T>(): MonoTypeOperatorFunction<T> 
    return s => !this.isRetryable() ? s : s.pipe(
      retryWhen(errors => errors.pipe(
        filter(response => 
          if (response.status !== TIMEOUT_EXCEPTION_CODE || 
              response.status !== UNKNOWN_EXCEPTION_CODE
          ) 
            throw response;
          
          return true;
          
        ),
        delay(this.saveCarRetryTimeout) // retry after X seconds
      )),
      timeout(this.saveCarProccessTime) // error after Y Seconds
    );
  

问题是当timeout(this.saveCarProccessTime) 抛出错误时会发生什么。它在源上调用unsubscribe,然后发出错误downstream

这意味着this.carService.saveCar(car) 需要一个取消订阅方法,该方法可以取消飞行中甚至最近完成的请求(由于异步内容可能会自行排序)。

你需要看看那里。

... 或

另一种可能的解决方案

永远不要取消进行中的请求。服务器拥有唯一的事实来源,始终假设 saveCar 可能仍然成功,如果您没有被告知 upstream(服务器)它已经失败。

在 y 秒后停止重试。你会得到另一个 504 Timeout,你可以直接把它扔给消费者来处理,不管他们怎么决定(大概和this.isRetryable() 返回 false 的方式一样)。

retryTimeoutLogic<T>(): MonoTypeOperatorFunction<T> 
  return s => !this.isRetryable() ? s : defer(() => 

    // The time when we stop retrying on TIMEOUT_EXCEPTION_CODE &
    // UNKNOWN_EXCEPTION_CODE. After this time we rethrow instead
    const timeoutDate = new Date().getMilliseconds() +  this.saveCarProccessTime
    
    return s.pipe(
      retryWhen(errors => errors.pipe(
        filter(response => 
          if (response.status !== TIMEOUT_EXCEPTION_CODE || 
              response.status !== UNKNOWN_EXCEPTION_CODE ||
              new Date().getMilliseconds() > timeoutDate
          ) 
            throw response;
          
          return true;
        ),
        delay(this.saveCarRetryTimeout) // retry after X seconds
      ))
    );
  )

【讨论】:

我注意到第二个代码块有问题。首先,if 条件需要有 If error 是 timeout 的逻辑,并且 timeout 还没有过去我们要继续。使用当前代码,如果其中任何一个为真,它将继续。其次,这段代码的目的不同——如果发送的第一个请求需要很多时间,它就不会重试,因为我没有任何超时。主要问题是服务工作者在超时时的行为(因为请求本身正在被取消)。也许将超时逻辑移到服务器端会更聪明? 是的,在这种情况下很好。另外,是的,第二个解决方案不希望让服务器在飞行中回滚更改。如果这是您想要的,您需要在服务器中实现和/或创建一个相当复杂的 savecar 服务(使用某种握手)。我没有想到任何替代方案。 我们目前决定听从您的建议,并使用没有超时的逻辑,以防止我们在保存时存在当前泄漏。谢谢! :)

以上是关于使用服务工作者正确地以角度方式超时 HTTP 请求的主要内容,如果未能解决你的问题,请参考以下文章

使用角度服务在多个组件中设置超时

HTTP超时问题

如何测试客户端代码是不是正确超时 API 请求到外部服务器

将正确的标头值发送到从角度 $http 到 asp.net webapi 的 Preflight 请求时出错

Dart Http 包请求超时

中止请求和超时 跨域的HTTP请求 认证方式 JSONP