1 /* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.odp.module.common.http; 18 19 import static com.android.odp.module.common.http.HttpClientUtils.HTTP_OK_STATUS; 20 21 import android.annotation.NonNull; 22 23 import com.google.common.annotations.VisibleForTesting; 24 import com.google.common.util.concurrent.Futures; 25 import com.google.common.util.concurrent.ListenableFuture; 26 import com.google.common.util.concurrent.ListeningExecutorService; 27 28 import java.io.IOException; 29 import java.util.concurrent.Callable; 30 31 /** 32 * The HTTP client to be used by FederatedCompute and ODP services/jobs to communicate with remote 33 * servers. 34 */ 35 public class HttpClient { 36 37 interface HttpIOSupplier<T> { get()38 T get() throws IOException; // Declared to throw IOException 39 } 40 41 private final int mRetryLimit; 42 43 /** The executor to use for making http requests. */ 44 private final ListeningExecutorService mBlockingExecutor; 45 HttpClient(int retryLimit, ListeningExecutorService blockingExecutor)46 public HttpClient(int retryLimit, ListeningExecutorService blockingExecutor) { 47 mRetryLimit = retryLimit; 48 mBlockingExecutor = blockingExecutor; 49 } 50 51 /** 52 * Perform HTTP requests based on given {@link OdpHttpRequest} asynchronously with configured 53 * number of retries. 54 * 55 * <p>Retry limit provided during construction is used in case http does not return {@code OK} 56 * response code. 57 */ 58 @NonNull performRequestAsyncWithRetry(OdpHttpRequest request)59 public ListenableFuture<OdpHttpResponse> performRequestAsyncWithRetry(OdpHttpRequest request) { 60 return performCallableAsync( 61 () -> performRequestWithRetry(() -> HttpClientUtils.performRequest(request))); 62 } 63 64 /** 65 * Perform HTTP requests based on given information asynchronously with retries in case http 66 * will return not OK response code. Payload will be saved directly into the file. 67 */ 68 @NonNull performRequestIntoFileAsyncWithRetry( OdpHttpRequest request)69 public ListenableFuture<OdpHttpResponse> performRequestIntoFileAsyncWithRetry( 70 OdpHttpRequest request) { 71 return performCallableAsync( 72 () -> performRequestWithRetry(() -> HttpClientUtils.performRequest(request, true))); 73 } 74 75 /** 76 * Perform HTTP requests based on given information asynchronously with retries in case http 77 * will return not OK response code. 78 */ 79 @NonNull performCallableAsync( Callable<OdpHttpResponse> callable)80 private ListenableFuture<OdpHttpResponse> performCallableAsync( 81 Callable<OdpHttpResponse> callable) { 82 try { 83 return mBlockingExecutor.submit(callable); 84 } catch (Exception e) { 85 return Futures.immediateFailedFuture(e); 86 } 87 } 88 89 /** Perform HTTP requests based on given information with retries. */ 90 @NonNull 91 @VisibleForTesting performRequestWithRetry(HttpIOSupplier<OdpHttpResponse> supplier)92 OdpHttpResponse performRequestWithRetry(HttpIOSupplier<OdpHttpResponse> supplier) 93 throws IOException { 94 OdpHttpResponse response = null; 95 int retryLimit = mRetryLimit; 96 while (retryLimit > 0) { 97 try { 98 response = supplier.get(); 99 if (HTTP_OK_STATUS.contains(response.getStatusCode())) { 100 return response; 101 } 102 // we want to continue retry in case it is IO exception. 103 } catch (IOException e) { 104 // propagate IO exception after RETRY_LIMIT times attempt. 105 if (retryLimit <= 1) { 106 throw e; 107 } 108 } finally { 109 retryLimit--; 110 } 111 } 112 return response; 113 } 114 } 115