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

Reply via email to