Skip to content

Commit 4c04563

Browse files
authored
fix: improve 429 retry handling with exponential backoff (#2524)
- Add exponential backoff for 429 responses without Retry-After header: starts at initial delay, doubles each retry, capped at 60s - Fix double-sleep bug: when Retry-After header present, code was sleeping both in the case match and in the retry logic. Now captures retryAfterMs before response.close() and uses it as the sole backoff delay - 429 retries remain unlimited (rate limiting is transient) - Add scalastyle:off for method length (sendWithRetries is necessarily long)
1 parent f906841 commit 4c04563

2 files changed

Lines changed: 342 additions & 9 deletions

File tree

core/src/main/scala/com/microsoft/azure/synapse/ml/io/http/HTTPClients.scala

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import org.apache.spark.sql.types.StringType
1616

1717
import scala.concurrent.duration.Duration
1818
import scala.concurrent.{ExecutionContext, blocking}
19-
import scala.util.Try
19+
import scala.util.{Random, Try}
2020

2121
trait 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

Comments
 (0)