Scala Play Framework是否存在后台HTTP请求重试机制?
我有两个基于Scala+Play Framework的应用,二者通过HTTP请求通信。场景如下:应用A接收外部(比如Postman)的请求后,仅负责转发到应用B,控制器代码如下:
SecuredAction.async(parse.json[JsObject]) { implicit request => implicit val user: User = request.identity withMetrics { appBclient.doCalculations(request.body, <some query params>) } toResult }
withMetrics方法实现如下:
protected def withMetrics(res: => Future[JsObject]): Future[JsObject] = { val timer = txScoreTimer.start() res.transform( { json => json ++ Json.obj("_api" -> Json.obj( "processingTimeSeconds" -> timer.observe)) }, { e: Throwable => val tookSeconds = timer.observe e match { case g: GeneralException => g.copy(processingTimeSeconds = Some(tookSeconds)) case _ => e } } ) }
由于应用B的计算任务耗时极长,应用A在120秒后触发超时异常:
java.util.concurrent.TimeoutException: Request timeout to host after 120000 ms at play.shaded.ahc.org.asynchttpclient.netty.timeout.TimeoutTimerTask.expire(TimeoutTimerTask.java:43) at play.shaded.ahc.org.asynchttpclient.netty.timeout.RequestTimeoutTimerTask.run(RequestTimeoutTimerTask.java:48) at play.shaded.ahc.io.netty.util.HashedWheelTimer$HashedWheelTimeout.expire(HashedWheelTimer.java:663) at play.shaded.ahc.io.netty.util.HashedWheelTimer$HashedWheelBucket.expireTimeouts(HashedWheelTimer.java:738) at play.shaded.ahc.io.netty.util.HashedWheelTimer$Worker.run(HashedWheelTimer.java:466) at java.lang.Thread.run(Thread.java:750)
但我代码里没写重试逻辑,应用B却在请求失败一段时间后收到了完全相同的请求。想问是不是应用环境变量或Play Framework本身的机制导致的?
Play内置HTTP客户端的自动重试:Play默认使用AsyncHttpClient(AHC)作为底层HTTP客户端,AHC本身带有默认重试策略。如果
application.conf中配置的play.ws.retries大于0,针对请求超时、连接超时这类异常就会触发自动重试。另外,AHC会根据HTTP方法判断是否重试,GET、HEAD这类幂等方法默认允许重试,POST方法若未明确禁用重试,也可能被触发。可以检查application.conf里的相关配置项:play.ws.timeout.request:请求超时时间(你的场景为120秒)play.ws.retries:重试次数play.ws.followRedirects:是否跟随重定向(部分重定向场景也会导致重复请求)
网络基础设施的重试机制:如果应用A和B之间存在负载均衡器、API网关或其他网络设备,这些设备可能配置了超时重试策略。当设备检测到请求超时后,会自动重新发送请求到应用B,这种情况属于网络层配置,和应用代码无关。
HTTP客户端内部隐式重试:检查
appBclient.doCalculations方法的实现,是否在客户端封装时隐式添加了重试逻辑,比如使用了RetryPolicy相关工具类,或者第三方HTTP客户端库自带了重试配置。Future按名参数的意外触发:你的
withMetrics方法中,res是按名参数(=> Future[JsObject]),虽然当前transform逻辑没有重新调用res,但如果后续异常处理逻辑中不小心重复触发了res的求值,就会导致重新发送请求。从现有代码看这个可能性较低,但可以排查异常处理分支是否有遗漏逻辑。
内容的提问来源于stack exchange,提问作者Zixel

