/* * Copyright 2014 Google Inc. All rights reserved. * * * Licensed 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 com.google.maps.internal; import com.google.gson.FieldNamingPolicy; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonSyntaxException; import com.google.maps.GeolocationApi; import com.google.maps.ImageResult; import com.google.maps.PendingResult; import com.google.maps.errors.ApiException; import com.google.maps.metrics.RequestMetrics; import com.google.maps.model.AddressComponentType; import com.google.maps.model.AddressType; import com.google.maps.model.Distance; import com.google.maps.model.Duration; import com.google.maps.model.Fare; import com.google.maps.model.LatLng; import com.google.maps.model.LocationType; import com.google.maps.model.OpeningHours.Period.OpenClose.DayOfWeek; import com.google.maps.model.PlaceDetails.Review.AspectRating.RatingType; import com.google.maps.model.PriceLevel; import com.google.maps.model.TravelMode; import java.io.IOException; import java.time.Instant; import java.time.LocalTime; import java.time.ZonedDateTime; import java.util.Arrays; import java.util.List; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import okhttp3.Call; import okhttp3.Callback; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import okhttp3.ResponseBody; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A PendingResult backed by a HTTP call executed by OkHttp, a deserialization step using Gson, rate * limiting and a retry policy. * * <p>{@code T} is the type of the result of this pending result, and {@code R} is the type of the * request. */ public class OkHttpPendingResult<T, R extends ApiResponse<T>> implements PendingResult<T>, Callback { private final Request request; private final OkHttpClient client; private final Class<R> responseClass; private final FieldNamingPolicy fieldNamingPolicy; private final Integer maxRetries; private final RequestMetrics metrics; private Call call; private Callback<T> callback; private long errorTimeOut; private int retryCounter = 0; private long cumulativeSleepTime = 0; private ExceptionsAllowedToRetry exceptionsAllowedToRetry; private static final Logger LOG = LoggerFactory.getLogger(OkHttpPendingResult.class.getName()); private static final List<Integer> RETRY_ERROR_CODES = Arrays.asList(500, 503, 504); /** * @param request HTTP request to execute. * @param client The client used to execute the request. * @param responseClass Model class to unmarshal JSON body content. * @param fieldNamingPolicy FieldNamingPolicy for unmarshaling JSON. * @param errorTimeOut Number of milliseconds to re-send erroring requests. * @param maxRetries Number of times allowed to re-send erroring requests. * @param exceptionsAllowedToRetry The exceptions to retry. */ public OkHttpPendingResult( Request request, OkHttpClient client, Class<R> responseClass, FieldNamingPolicy fieldNamingPolicy, long errorTimeOut, Integer maxRetries, ExceptionsAllowedToRetry exceptionsAllowedToRetry, RequestMetrics metrics) { this.request = request; this.client = client; this.responseClass = responseClass; this.fieldNamingPolicy = fieldNamingPolicy; this.errorTimeOut = errorTimeOut; this.maxRetries = maxRetries; this.exceptionsAllowedToRetry = exceptionsAllowedToRetry; this.metrics = metrics; metrics.startNetwork(); this.call = client.newCall(request); } @Override public void setCallback(Callback<T> callback) { this.callback = callback; call.enqueue(this); } /** Preserve a request/response pair through an asynchronous callback. */ private class QueuedResponse { private final OkHttpPendingResult<T, R> request; private final Response response; private final IOException e; public QueuedResponse(OkHttpPendingResult<T, R> request, Response response) { this.request = request; this.response = response; this.e = null; } public QueuedResponse(OkHttpPendingResult<T, R> request, IOException e) { this.request = request; this.response = null; this.e = e; } } @Override public T await() throws ApiException, IOException, InterruptedException { // Handle sleeping for retried requests if (retryCounter > 0) { // 0.5 * (1.5 ^ i) represents an increased sleep time of 1.5x per iteration, // starting at 0.5s when i = 0. The retryCounter will be 1 for the 1st retry, // so subtract 1 here. double delaySecs = 0.5 * Math.pow(1.5, retryCounter - 1); // Generate a jitter value between -delaySecs / 2 and +delaySecs / 2 long delayMillis = (long) (delaySecs * (Math.random() + 0.5) * 1000); LOG.debug( String.format( "Sleeping between errors for %dms (retry #%d, already slept %dms)", delayMillis, retryCounter, cumulativeSleepTime)); cumulativeSleepTime += delayMillis; try { Thread.sleep(delayMillis); } catch (InterruptedException e) { // No big deal if we don't sleep as long as intended. } } final BlockingQueue<QueuedResponse> waiter = new ArrayBlockingQueue<>(1); final OkHttpPendingResult<T, R> parent = this; // This callback will be called on another thread, handled by the RateLimitExecutorService. // Calling call.execute() directly would bypass the rate limiting. call.enqueue( new okhttp3.Callback() { @Override public void onFailure(Call call, IOException e) { metrics.endNetwork(); waiter.add(new QueuedResponse(parent, e)); } @Override public void onResponse(Call call, Response response) throws IOException { metrics.endNetwork(); waiter.add(new QueuedResponse(parent, response)); } }); QueuedResponse r = waiter.take(); if (r.response != null) { return parseResponse(r.request, r.response); } else { metrics.endRequest(r.e, 0, retryCounter); throw r.e; } } @Override public T awaitIgnoreError() { try { return await(); } catch (Exception e) { return null; } } @Override public void cancel() { call.cancel(); } @Override public void onFailure(Call call, IOException ioe) { metrics.endNetwork(); if (callback != null) { metrics.endRequest(ioe, 0, retryCounter); callback.onFailure(ioe); } } @Override public void onResponse(Call call, Response response) throws IOException { metrics.endNetwork(); if (callback != null) { try { callback.onResult(parseResponse(this, response)); } catch (Exception e) { callback.onFailure(e); } } } @SuppressWarnings("unchecked") private T parseResponse(OkHttpPendingResult<T, R> request, Response response) throws ApiException, InterruptedException, IOException { try { T result = parseResponseInternal(request, response); metrics.endRequest(null, response.code(), retryCounter); return result; } catch (Exception e) { metrics.endRequest(e, response.code(), retryCounter); throw e; } } @SuppressWarnings("unchecked") private T parseResponseInternal(OkHttpPendingResult<T, R> request, Response response) throws ApiException, InterruptedException, IOException { if (shouldRetry(response)) { // since we are retrying the request we must close the response response.close(); // Retry is a blocking method, but that's OK. If we're here, we're either in an await() // call, which is blocking anyway, or we're handling a callback in a separate thread. return request.retry(); } byte[] bytes; try (ResponseBody body = response.body()) { bytes = body.bytes(); } R resp; String contentType = response.header("Content-Type"); if (contentType != null && contentType.startsWith("image") && responseClass == ImageResult.Response.class && response.code() == 200) { ImageResult result = new ImageResult(contentType, bytes); return (T) result; } Gson gson = new GsonBuilder() .registerTypeAdapter(ZonedDateTime.class, new ZonedDateTimeAdapter()) .registerTypeAdapter(Distance.class, new DistanceAdapter()) .registerTypeAdapter(Duration.class, new DurationAdapter()) .registerTypeAdapter(Fare.class, new FareAdapter()) .registerTypeAdapter(LatLng.class, new LatLngAdapter()) .registerTypeAdapter( AddressComponentType.class, new SafeEnumAdapter<AddressComponentType>(AddressComponentType.UNKNOWN)) .registerTypeAdapter( AddressType.class, new SafeEnumAdapter<AddressType>(AddressType.UNKNOWN)) .registerTypeAdapter( TravelMode.class, new SafeEnumAdapter<TravelMode>(TravelMode.UNKNOWN)) .registerTypeAdapter( LocationType.class, new SafeEnumAdapter<LocationType>(LocationType.UNKNOWN)) .registerTypeAdapter( RatingType.class, new SafeEnumAdapter<RatingType>(RatingType.UNKNOWN)) .registerTypeAdapter(DayOfWeek.class, new DayOfWeekAdapter()) .registerTypeAdapter(PriceLevel.class, new PriceLevelAdapter()) .registerTypeAdapter(Instant.class, new InstantAdapter()) .registerTypeAdapter(LocalTime.class, new LocalTimeAdapter()) .registerTypeAdapter(GeolocationApi.Response.class, new GeolocationResponseAdapter()) .setFieldNamingPolicy(fieldNamingPolicy) .create(); // Attempt to de-serialize before checking the HTTP status code, as there may be JSON in the // body that we can use to provide a more descriptive exception. try { resp = gson.fromJson(new String(bytes, "utf8"), responseClass); } catch (JsonSyntaxException e) { // Check HTTP status for a more suitable exception if (!response.isSuccessful()) { // Some of the APIs return 200 even when the API request fails, as long as the transport // mechanism succeeds. In these cases, INVALID_RESPONSE, etc are handled by the Gson // parsing. throw new IOException( String.format("Server Error: %d %s", response.code(), response.message())); } // Otherwise just cough up the syntax exception. throw e; } if (resp.successful()) { // Return successful responses return resp.getResult(); } else { ApiException e = resp.getError(); if (shouldRetry(e)) { return request.retry(); } else { throw e; } } } private T retry() throws ApiException, InterruptedException, IOException { retryCounter++; LOG.info("Retrying request. Retry #" + retryCounter); metrics.startNetwork(); this.call = client.newCall(request); return this.await(); } private boolean shouldRetry(Response response) { return RETRY_ERROR_CODES.contains(response.code()) && cumulativeSleepTime < errorTimeOut && (maxRetries == null || retryCounter < maxRetries); } private boolean shouldRetry(ApiException exception) { return exceptionsAllowedToRetry.contains(exception.getClass()) && cumulativeSleepTime < errorTimeOut && (maxRetries == null || retryCounter < maxRetries); } }