@@ -16,7 +16,7 @@ import org.apache.spark.sql.types.StringType
1616
1717import scala .concurrent .duration .Duration
1818import scala .concurrent .{ExecutionContext , blocking }
19- import scala .util .Try
19+ import scala .util .{ Random , Try }
2020
2121trait Handler {
2222
@@ -74,20 +74,26 @@ object HandlingUtils extends SparkLogging {
7474 private def keepTrying (client : CloseableHttpClient ,
7575 request : HttpRequestBase ,
7676 retriesLeft : Array [Int ],
77- e : Throwable ): CloseableHttpResponse = {
77+ e : Throwable ,
78+ extraCodesToRetry : Set [Int ] = Set (),
79+ backoff429Ms : Long = 0 ): CloseableHttpResponse = {
7880 if (retriesLeft.isEmpty) {
7981 throw e
8082 } else {
8183 Thread .sleep(retriesLeft.head.toLong)
82- sendWithRetries(client, request, retriesLeft.tail)
84+ sendWithRetries(client, request, retriesLeft.tail, extraCodesToRetry, backoff429Ms )
8385 }
8486 }
8587
88+ private val MaxBackoffMs : Long = 60000L // 1 minute cap for 429 backoff
89+
8690 // scalastyle:off cyclomatic.complexity
91+ // scalastyle:off method.length
8792 private [ml] def sendWithRetries (client : CloseableHttpClient ,
8893 request : HttpRequestBase ,
8994 retriesLeft : Array [Int ],
90- extraCodesToRetry : Set [Int ] = Set ()
95+ extraCodesToRetry : Set [Int ] = Set (),
96+ backoff429Ms : Long = 0
9197 ): CloseableHttpResponse = {
9298 try {
9399 val response = client.execute(request)
@@ -107,7 +113,6 @@ object HandlingUtils extends SparkLogging {
107113 case _ => request.getURI
108114 }
109115 }" )
110- Thread .sleep(h.getValue.toLong * 1000 )
111116 }
112117 false
113118 case code =>
@@ -129,20 +134,42 @@ object HandlingUtils extends SparkLogging {
129134 if (dontRetry || retriesLeft.isEmpty) {
130135 response
131136 } else {
137+ // Retry-After may be delta-seconds (e.g. "120") or an HTTP-date.
138+ // Parse as Long; if it's an HTTP-date or otherwise non-numeric, fall back to exponential backoff.
139+ // Cap server-provided values to MaxBackoffMs; Retry-After: 0 means "retry immediately" (RFC 7231).
140+ val retryAfterMs = if (code == 429 ) {
141+ Option (response.getFirstHeader(" Retry-After" ))
142+ .flatMap(h => Try (h.getValue.toLong * 1000 ).toOption)
143+ .filter(_ >= 0 )
144+ .map(math.min(_, MaxBackoffMs ))
145+ } else None
132146 response.close()
133- Thread .sleep(retriesLeft.head.toLong)
134147 if (code == 429 ) { // Do not count rate limiting in number of failures
135- sendWithRetries(client, request, retriesLeft)
148+ val baseBackoff = retryAfterMs.getOrElse {
149+ // No usable Retry-After header; use exponential backoff
150+ val current = math.max(backoff429Ms, retriesLeft.head.toLong)
151+ math.min(current * 2 , MaxBackoffMs )
152+ }
153+ // Add jitter (up to 10% of base) to prevent thundering herd
154+ val jitter = Random .nextInt(math.max((baseBackoff / 10 ).toInt, 1 ))
155+ val sleepMs = math.min(baseBackoff + jitter, MaxBackoffMs )
156+ val source = if (retryAfterMs.isDefined) " Retry-After header" else " exponential backoff"
157+ logInfo(s " 429 rate-limited on ${request.getURI}: " +
158+ s " sleeping ${sleepMs}ms ( $source, jitter= ${jitter}ms) " )
159+ Thread .sleep(sleepMs)
160+ sendWithRetries(client, request, retriesLeft, extraCodesToRetry, baseBackoff)
136161 } else {
137- sendWithRetries(client, request, retriesLeft.tail)
162+ Thread .sleep(retriesLeft.head.toLong)
163+ sendWithRetries(client, request, retriesLeft.tail, extraCodesToRetry)
138164 }
139165 }
140166 } catch {
141167 case e : java.io.IOException =>
142168 logError(" Encountering a connection error" , e)
143- keepTrying(client, request, retriesLeft, e)
169+ keepTrying(client, request, retriesLeft, e, extraCodesToRetry, backoff429Ms )
144170 }
145171 }
172+ // scalastyle:on method.length
146173 // scalastyle:on cyclomatic.complexity
147174
148175 def advanced (retryTimes : Int * )(client : CloseableHttpClient ,
0 commit comments