This is an automated email from the ASF dual-hosted git repository.
blue pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iceberg.git
The following commit(s) were added to refs/heads/master by this push:
new 3a3c6eddbc Core: Default to exponential retry strategy in REST client
(#8366)
3a3c6eddbc is described below
commit 3a3c6eddbc7f1da1c4d5934e63b67af16cff29d1
Author: Eduard Tudenhoefner <[email protected]>
AuthorDate: Fri Sep 15 00:31:40 2023 +0200
Core: Default to exponential retry strategy in REST client (#8366)
---
LICENSE | 10 ++
.../rest/ExponentialHttpRequestRetryStrategy.java | 151 ++++++++++++++++
.../java/org/apache/iceberg/rest/HTTPClient.java | 10 +-
.../TestExponentialHttpRequestRetryStrategy.java | 199 +++++++++++++++++++++
flink/v1.15/flink-runtime/LICENSE | 10 ++
flink/v1.16/flink-runtime/LICENSE | 10 ++
flink/v1.17/flink-runtime/LICENSE | 10 ++
hive-runtime/LICENSE | 10 ++
spark/v3.1/spark-runtime/LICENSE | 10 ++
spark/v3.2/spark-runtime/LICENSE | 10 ++
spark/v3.3/spark-runtime/LICENSE | 10 ++
spark/v3.4/spark-runtime/LICENSE | 10 ++
12 files changed, 448 insertions(+), 2 deletions(-)
diff --git a/LICENSE b/LICENSE
index 515fd54d6e..3b4bb2ed63 100644
--- a/LICENSE
+++ b/LICENSE
@@ -313,3 +313,13 @@ This product includes code from Apache Commons.
Copyright: 2020 The Apache Software Foundation
Home page: https://commons.apache.org/
License: https://www.apache.org/licenses/LICENSE-2.0
+
+--------------------------------------------------------------------------------
+
+This product includes code from Apache HttpComponents Client.
+
+* retry and error handling logic in ExponentialHttpRequestRetryStrategy.java
+
+Copyright: 1999-2022 The Apache Software Foundation.
+Home page: https://hc.apache.org/
+License: https://www.apache.org/licenses/LICENSE-2.0
diff --git
a/core/src/main/java/org/apache/iceberg/rest/ExponentialHttpRequestRetryStrategy.java
b/core/src/main/java/org/apache/iceberg/rest/ExponentialHttpRequestRetryStrategy.java
new file mode 100644
index 0000000000..9d8f5424f5
--- /dev/null
+++
b/core/src/main/java/org/apache/iceberg/rest/ExponentialHttpRequestRetryStrategy.java
@@ -0,0 +1,151 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.rest;
+
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.net.ConnectException;
+import java.net.NoRouteToHostException;
+import java.net.UnknownHostException;
+import java.time.Instant;
+import java.util.Set;
+import java.util.concurrent.ThreadLocalRandom;
+import javax.net.ssl.SSLException;
+import org.apache.hc.client5.http.HttpRequestRetryStrategy;
+import org.apache.hc.client5.http.utils.DateUtils;
+import org.apache.hc.core5.concurrent.CancellableDependency;
+import org.apache.hc.core5.http.ConnectionClosedException;
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.HttpHeaders;
+import org.apache.hc.core5.http.HttpRequest;
+import org.apache.hc.core5.http.HttpResponse;
+import org.apache.hc.core5.http.HttpStatus;
+import org.apache.hc.core5.http.Method;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.util.TimeValue;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableSet;
+
+/**
+ * Defines an exponential HTTP request retry strategy and provides the same
characteristics as the
+ * {@link org.apache.hc.client5.http.impl.DefaultHttpRequestRetryStrategy},
using the following list
+ * of non-retriable I/O exception classes:
+ *
+ * <ul>
+ * <li>InterruptedIOException
+ * <li>UnknownHostException
+ * <li>ConnectException
+ * <li>ConnectionClosedException
+ * <li>NoRouteToHostException
+ * <li>SSLException
+ * </ul>
+ *
+ * The following retriable HTTP status codes are defined:
+ *
+ * <ul>
+ * <li>SC_TOO_MANY_REQUESTS (429)
+ * <li>SC_SERVICE_UNAVAILABLE (503)
+ * </ul>
+ *
+ * Most code and behavior is taken from {@link
+ * org.apache.hc.client5.http.impl.DefaultHttpRequestRetryStrategy}, with
minor modifications to
+ * {@link #getRetryInterval(HttpResponse, int, HttpContext)} to achieve
exponential backoff.
+ */
+class ExponentialHttpRequestRetryStrategy implements HttpRequestRetryStrategy {
+ private final int maxRetries;
+ private final Set<Class<? extends IOException>> nonRetriableExceptions;
+ private final Set<Integer> retriableCodes;
+
+ ExponentialHttpRequestRetryStrategy(int maximumRetries) {
+ Preconditions.checkArgument(
+ maximumRetries > 0, "Cannot set retries to %s, the value must be
positive", maximumRetries);
+ this.maxRetries = maximumRetries;
+ this.retriableCodes =
+ ImmutableSet.of(HttpStatus.SC_TOO_MANY_REQUESTS,
HttpStatus.SC_SERVICE_UNAVAILABLE);
+ this.nonRetriableExceptions =
+ ImmutableSet.of(
+ InterruptedIOException.class,
+ UnknownHostException.class,
+ ConnectException.class,
+ ConnectionClosedException.class,
+ NoRouteToHostException.class,
+ SSLException.class);
+ }
+
+ @Override
+ public boolean retryRequest(
+ HttpRequest request, IOException exception, int execCount, HttpContext
context) {
+ if (execCount > maxRetries) {
+ // Do not retry if over max retries
+ return false;
+ }
+
+ if (nonRetriableExceptions.contains(exception.getClass())) {
+ return false;
+ } else {
+ for (Class<? extends IOException> rejectException :
nonRetriableExceptions) {
+ if (rejectException.isInstance(exception)) {
+ return false;
+ }
+ }
+ }
+
+ if (request instanceof CancellableDependency
+ && ((CancellableDependency) request).isCancelled()) {
+ return false;
+ }
+
+ // Retry if the request is considered idempotent
+ return Method.isIdempotent(request.getMethod());
+ }
+
+ @Override
+ public boolean retryRequest(HttpResponse response, int execCount,
HttpContext context) {
+ return execCount <= maxRetries &&
retriableCodes.contains(response.getCode());
+ }
+
+ @Override
+ public TimeValue getRetryInterval(HttpResponse response, int execCount,
HttpContext context) {
+ // a server may send a 429 / 503 with a Retry-After header
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
+ Header header = response.getFirstHeader(HttpHeaders.RETRY_AFTER);
+ TimeValue retryAfter = null;
+ if (header != null) {
+ String value = header.getValue();
+ try {
+ retryAfter = TimeValue.ofSeconds(Long.parseLong(value));
+ } catch (NumberFormatException ignore) {
+ Instant retryAfterDate = DateUtils.parseStandardDate(value);
+ if (retryAfterDate != null) {
+ retryAfter =
+ TimeValue.ofMilliseconds(retryAfterDate.toEpochMilli() -
System.currentTimeMillis());
+ }
+ }
+
+ if (TimeValue.isPositive(retryAfter)) {
+ return retryAfter;
+ }
+ }
+
+ int delayMillis = 1000 * (int) Math.min(Math.pow(2.0, (long) execCount -
1), 64.0);
+ int jitter = ThreadLocalRandom.current().nextInt(Math.max(1, (int)
(delayMillis * 0.1)));
+
+ return TimeValue.ofMilliseconds(delayMillis + jitter);
+ }
+}
diff --git a/core/src/main/java/org/apache/iceberg/rest/HTTPClient.java
b/core/src/main/java/org/apache/iceberg/rest/HTTPClient.java
index b2b6fc8a7c..e1e5637f23 100644
--- a/core/src/main/java/org/apache/iceberg/rest/HTTPClient.java
+++ b/core/src/main/java/org/apache/iceberg/rest/HTTPClient.java
@@ -71,6 +71,8 @@ public class HTTPClient implements RESTClient {
@VisibleForTesting
static final String CLIENT_GIT_COMMIT_SHORT_HEADER =
"X-Client-Git-Commit-Short";
+ private static final String REST_MAX_RETRIES = "rest.client.max-retries";
+
private final String uri;
private final CloseableHttpClient httpClient;
private final ObjectMapper mapper;
@@ -79,7 +81,8 @@ public class HTTPClient implements RESTClient {
String uri,
Map<String, String> baseHeaders,
ObjectMapper objectMapper,
- HttpRequestInterceptor requestInterceptor) {
+ HttpRequestInterceptor requestInterceptor,
+ Map<String, String> properties) {
this.uri = uri;
this.mapper = objectMapper;
@@ -96,6 +99,9 @@ public class HTTPClient implements RESTClient {
clientBuilder.addRequestInterceptorLast(requestInterceptor);
}
+ int maxRetries = PropertyUtil.propertyAsInt(properties, REST_MAX_RETRIES,
5);
+ clientBuilder.setRetryStrategy(new
ExponentialHttpRequestRetryStrategy(maxRetries));
+
this.httpClient = clientBuilder.build();
}
@@ -466,7 +472,7 @@ public class HTTPClient implements RESTClient {
interceptor =
loadInterceptorDynamically(SIGV4_REQUEST_INTERCEPTOR_IMPL, properties);
}
- return new HTTPClient(uri, baseHeaders, mapper, interceptor);
+ return new HTTPClient(uri, baseHeaders, mapper, interceptor, properties);
}
}
diff --git
a/core/src/test/java/org/apache/iceberg/rest/TestExponentialHttpRequestRetryStrategy.java
b/core/src/test/java/org/apache/iceberg/rest/TestExponentialHttpRequestRetryStrategy.java
new file mode 100644
index 0000000000..e63bdfd067
--- /dev/null
+++
b/core/src/test/java/org/apache/iceberg/rest/TestExponentialHttpRequestRetryStrategy.java
@@ -0,0 +1,199 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.rest;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.net.ConnectException;
+import java.net.NoRouteToHostException;
+import java.net.SocketTimeoutException;
+import java.net.UnknownHostException;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import javax.net.ssl.SSLException;
+import org.apache.hc.client5.http.HttpRequestRetryStrategy;
+import org.apache.hc.client5.http.classic.methods.HttpGet;
+import org.apache.hc.client5.http.utils.DateUtils;
+import org.apache.hc.core5.http.ConnectionClosedException;
+import org.apache.hc.core5.http.HttpHeaders;
+import org.apache.hc.core5.http.HttpResponse;
+import org.apache.hc.core5.http.message.BasicHttpResponse;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+public class TestExponentialHttpRequestRetryStrategy {
+
+ private final HttpRequestRetryStrategy retryStrategy = new
ExponentialHttpRequestRetryStrategy(5);
+
+ @ParameterizedTest
+ @ValueSource(ints = {-1, 0})
+ public void invalidRetries(int retries) {
+ assertThatThrownBy(() -> new ExponentialHttpRequestRetryStrategy(retries))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage(String.format("Cannot set retries to %s, the value must be
positive", retries));
+ }
+
+ @Test
+ public void exponentialRetry() {
+ HttpRequestRetryStrategy strategy = new
ExponentialHttpRequestRetryStrategy(10);
+ BasicHttpResponse response = new BasicHttpResponse(503, "Oopsie");
+
+ // note that the upper limit includes ~10% variability
+ assertThat(strategy.getRetryInterval(response, 0,
null).toMilliseconds()).isEqualTo(0);
+ assertThat(strategy.getRetryInterval(response, 1, null).toMilliseconds())
+ .isBetween(1000L, 2000L);
+ assertThat(strategy.getRetryInterval(response, 2, null).toMilliseconds())
+ .isBetween(2000L, 3000L);
+ assertThat(strategy.getRetryInterval(response, 3, null).toMilliseconds())
+ .isBetween(4000L, 5000L);
+ assertThat(strategy.getRetryInterval(response, 4, null).toMilliseconds())
+ .isBetween(8000L, 9000L);
+ assertThat(strategy.getRetryInterval(response, 5, null).toMilliseconds())
+ .isBetween(16000L, 18000L);
+ assertThat(strategy.getRetryInterval(response, 6, null).toMilliseconds())
+ .isBetween(32000L, 36000L);
+ assertThat(strategy.getRetryInterval(response, 7, null).toMilliseconds())
+ .isBetween(64000L, 72000L);
+ assertThat(strategy.getRetryInterval(response, 10, null).toMilliseconds())
+ .isBetween(64000L, 72000L);
+ }
+
+ @Test
+ public void basicRetry() {
+ BasicHttpResponse response503 = new BasicHttpResponse(503, "Oopsie");
+ assertThat(retryStrategy.retryRequest(response503, 3, null)).isTrue();
+
+ BasicHttpResponse response429 = new BasicHttpResponse(429, "Oopsie");
+ assertThat(retryStrategy.retryRequest(response429, 3, null)).isTrue();
+
+ BasicHttpResponse response404 = new BasicHttpResponse(404, "Oopsie");
+ assertThat(retryStrategy.retryRequest(response404, 3, null)).isFalse();
+ }
+
+ @Test
+ public void noRetryOnConnectTimeout() {
+ HttpGet request = new HttpGet("/");
+
+ assertThat(retryStrategy.retryRequest(request, new
SocketTimeoutException(), 1, null))
+ .isFalse();
+ }
+
+ @Test
+ public void noRetryOnConnect() {
+ HttpGet request = new HttpGet("/");
+
+ assertThat(retryStrategy.retryRequest(request, new ConnectException(), 1,
null)).isFalse();
+ }
+
+ @Test
+ public void noRetryOnConnectionClosed() {
+ HttpGet request = new HttpGet("/");
+
+ assertThat(retryStrategy.retryRequest(request, new
ConnectionClosedException(), 1, null))
+ .isFalse();
+ }
+
+ @Test
+ public void noRetryForNoRouteToHostException() {
+ HttpGet request = new HttpGet("/");
+
+ assertThat(retryStrategy.retryRequest(request, new
NoRouteToHostException(), 1, null))
+ .isFalse();
+ }
+
+ @Test
+ public void noRetryOnSSLFailure() {
+ HttpGet request = new HttpGet("/");
+
+ assertThat(retryStrategy.retryRequest(request, new
SSLException("encryption failed"), 1, null))
+ .isFalse();
+ }
+
+ @Test
+ public void noRetryOnUnknownHost() {
+ HttpGet request = new HttpGet("/");
+
+ assertThat(retryStrategy.retryRequest(request, new UnknownHostException(),
1, null)).isFalse();
+ }
+
+ @Test
+ public void noRetryOnInterruptedFailure() {
+ HttpGet request = new HttpGet("/");
+
+ assertThat(retryStrategy.retryRequest(request, new
InterruptedIOException(), 1, null))
+ .isFalse();
+ }
+
+ @Test
+ public void noRetryOnAbortedRequests() {
+ HttpGet request = new HttpGet("/");
+ request.cancel();
+
+ assertThat(retryStrategy.retryRequest(request, new IOException(), 1,
null)).isFalse();
+ }
+
+ @Test
+ public void retryOnNonAbortedRequests() {
+ HttpGet request = new HttpGet("/");
+
+ assertThat(retryStrategy.retryRequest(request, new IOException(), 1,
null)).isTrue();
+ }
+
+ @Test
+ public void retryAfterHeaderAsLong() {
+ HttpResponse response = new BasicHttpResponse(503, "Oopsie");
+ response.setHeader(HttpHeaders.RETRY_AFTER, "321");
+
+ assertThat(retryStrategy.getRetryInterval(response, 3,
null).toSeconds()).isEqualTo(321L);
+ }
+
+ @Test
+ public void retryAfterHeaderAsDate() {
+ HttpResponse response = new BasicHttpResponse(503, "Oopsie");
+ response.setHeader(
+ HttpHeaders.RETRY_AFTER,
+ DateUtils.formatStandardDate(Instant.now().plus(100,
ChronoUnit.SECONDS)));
+
+ assertThat(retryStrategy.getRetryInterval(response, 3,
null).toSeconds()).isBetween(0L, 100L);
+ }
+
+ @Test
+ public void retryAfterHeaderAsPastDate() {
+ HttpResponse response = new BasicHttpResponse(503, "Oopsie");
+ response.setHeader(
+ HttpHeaders.RETRY_AFTER,
+ DateUtils.formatStandardDate(Instant.now().minus(100,
ChronoUnit.SECONDS)));
+
+ assertThat(retryStrategy.getRetryInterval(response, 3,
null).toMilliseconds())
+ .isBetween(4000L, 5000L);
+ }
+
+ @Test
+ public void invalidRetryAfterHeader() {
+ HttpResponse response = new BasicHttpResponse(503, "Oopsie");
+ response.setHeader(HttpHeaders.RETRY_AFTER, "Stuff");
+
+ assertThat(retryStrategy.getRetryInterval(response, 3,
null).toMilliseconds())
+ .isBetween(4000L, 5000L);
+ }
+}
diff --git a/flink/v1.15/flink-runtime/LICENSE
b/flink/v1.15/flink-runtime/LICENSE
index a6161156db..8ab53469eb 100644
--- a/flink/v1.15/flink-runtime/LICENSE
+++ b/flink/v1.15/flink-runtime/LICENSE
@@ -490,3 +490,13 @@ This binary artifact contains Apache HttpComponents Client.
Copyright: 1999-2022 The Apache Software Foundation.
Home page: https://hc.apache.org/
License: http://www.apache.org/licenses/LICENSE-2.0
+
+--------------------------------------------------------------------------------
+
+This product includes code from Apache HttpComponents Client.
+
+* retry and error handling logic in ExponentialHttpRequestRetryStrategy.java
+
+Copyright: 1999-2022 The Apache Software Foundation.
+Home page: https://hc.apache.org/
+License: https://www.apache.org/licenses/LICENSE-2.0
diff --git a/flink/v1.16/flink-runtime/LICENSE
b/flink/v1.16/flink-runtime/LICENSE
index a6161156db..8ab53469eb 100644
--- a/flink/v1.16/flink-runtime/LICENSE
+++ b/flink/v1.16/flink-runtime/LICENSE
@@ -490,3 +490,13 @@ This binary artifact contains Apache HttpComponents Client.
Copyright: 1999-2022 The Apache Software Foundation.
Home page: https://hc.apache.org/
License: http://www.apache.org/licenses/LICENSE-2.0
+
+--------------------------------------------------------------------------------
+
+This product includes code from Apache HttpComponents Client.
+
+* retry and error handling logic in ExponentialHttpRequestRetryStrategy.java
+
+Copyright: 1999-2022 The Apache Software Foundation.
+Home page: https://hc.apache.org/
+License: https://www.apache.org/licenses/LICENSE-2.0
diff --git a/flink/v1.17/flink-runtime/LICENSE
b/flink/v1.17/flink-runtime/LICENSE
index a6161156db..8ab53469eb 100644
--- a/flink/v1.17/flink-runtime/LICENSE
+++ b/flink/v1.17/flink-runtime/LICENSE
@@ -490,3 +490,13 @@ This binary artifact contains Apache HttpComponents Client.
Copyright: 1999-2022 The Apache Software Foundation.
Home page: https://hc.apache.org/
License: http://www.apache.org/licenses/LICENSE-2.0
+
+--------------------------------------------------------------------------------
+
+This product includes code from Apache HttpComponents Client.
+
+* retry and error handling logic in ExponentialHttpRequestRetryStrategy.java
+
+Copyright: 1999-2022 The Apache Software Foundation.
+Home page: https://hc.apache.org/
+License: https://www.apache.org/licenses/LICENSE-2.0
diff --git a/hive-runtime/LICENSE b/hive-runtime/LICENSE
index a1b785a148..24cd3612e5 100644
--- a/hive-runtime/LICENSE
+++ b/hive-runtime/LICENSE
@@ -490,3 +490,13 @@ This binary artifact contains Apache HttpComponents Client.
Copyright: 1999-2022 The Apache Software Foundation.
Home page: https://hc.apache.org/
License: http://www.apache.org/licenses/LICENSE-2.0
+
+--------------------------------------------------------------------------------
+
+This product includes code from Apache HttpComponents Client.
+
+* retry and error handling logic in ExponentialHttpRequestRetryStrategy.java
+
+Copyright: 1999-2022 The Apache Software Foundation.
+Home page: https://hc.apache.org/
+License: https://www.apache.org/licenses/LICENSE-2.0
diff --git a/spark/v3.1/spark-runtime/LICENSE b/spark/v3.1/spark-runtime/LICENSE
index 39ac85f1bd..801bcf0e4c 100644
--- a/spark/v3.1/spark-runtime/LICENSE
+++ b/spark/v3.1/spark-runtime/LICENSE
@@ -626,3 +626,13 @@ This binary artifact contains Apache HttpComponents Client.
Copyright: 1999-2022 The Apache Software Foundation.
Home page: https://hc.apache.org/
License: http://www.apache.org/licenses/LICENSE-2.0
+
+--------------------------------------------------------------------------------
+
+This product includes code from Apache HttpComponents Client.
+
+* retry and error handling logic in ExponentialHttpRequestRetryStrategy.java
+
+Copyright: 1999-2022 The Apache Software Foundation.
+Home page: https://hc.apache.org/
+License: https://www.apache.org/licenses/LICENSE-2.0
diff --git a/spark/v3.2/spark-runtime/LICENSE b/spark/v3.2/spark-runtime/LICENSE
index 9d15224316..1d3e877720 100644
--- a/spark/v3.2/spark-runtime/LICENSE
+++ b/spark/v3.2/spark-runtime/LICENSE
@@ -627,3 +627,13 @@ This binary artifact contains Apache HttpComponents Client.
Copyright: 1999-2022 The Apache Software Foundation.
Home page: https://hc.apache.org/
License: http://www.apache.org/licenses/LICENSE-2.0
+
+--------------------------------------------------------------------------------
+
+This product includes code from Apache HttpComponents Client.
+
+* retry and error handling logic in ExponentialHttpRequestRetryStrategy.java
+
+Copyright: 1999-2022 The Apache Software Foundation.
+Home page: https://hc.apache.org/
+License: https://www.apache.org/licenses/LICENSE-2.0
diff --git a/spark/v3.3/spark-runtime/LICENSE b/spark/v3.3/spark-runtime/LICENSE
index 9d15224316..1d3e877720 100644
--- a/spark/v3.3/spark-runtime/LICENSE
+++ b/spark/v3.3/spark-runtime/LICENSE
@@ -627,3 +627,13 @@ This binary artifact contains Apache HttpComponents Client.
Copyright: 1999-2022 The Apache Software Foundation.
Home page: https://hc.apache.org/
License: http://www.apache.org/licenses/LICENSE-2.0
+
+--------------------------------------------------------------------------------
+
+This product includes code from Apache HttpComponents Client.
+
+* retry and error handling logic in ExponentialHttpRequestRetryStrategy.java
+
+Copyright: 1999-2022 The Apache Software Foundation.
+Home page: https://hc.apache.org/
+License: https://www.apache.org/licenses/LICENSE-2.0
diff --git a/spark/v3.4/spark-runtime/LICENSE b/spark/v3.4/spark-runtime/LICENSE
index 9d15224316..1d3e877720 100644
--- a/spark/v3.4/spark-runtime/LICENSE
+++ b/spark/v3.4/spark-runtime/LICENSE
@@ -627,3 +627,13 @@ This binary artifact contains Apache HttpComponents Client.
Copyright: 1999-2022 The Apache Software Foundation.
Home page: https://hc.apache.org/
License: http://www.apache.org/licenses/LICENSE-2.0
+
+--------------------------------------------------------------------------------
+
+This product includes code from Apache HttpComponents Client.
+
+* retry and error handling logic in ExponentialHttpRequestRetryStrategy.java
+
+Copyright: 1999-2022 The Apache Software Foundation.
+Home page: https://hc.apache.org/
+License: https://www.apache.org/licenses/LICENSE-2.0