package com.example.api; import java.io.IOException; import java.lang.annotation.Annotation; import java.lang.reflect.Type; import java.util.Random; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import retrofit.Call; import retrofit.CallAdapter; import retrofit.Callback; import retrofit.Response; import retrofit.Retrofit; /** * Retries calls marked with {@link Retry}. */ public class RetryCallAdapterFactory implements CallAdapter.Factory { private final ScheduledExecutorService mExecutor; private RetryCallAdapterFactory() { mExecutor = Executors.newScheduledThreadPool(1); } public static RetryCallAdapterFactory create() { return new RetryCallAdapterFactory(); } @Override public CallAdapter get(final Type returnType, Annotation[] annotations, Retrofit retrofit) { boolean hasRetryAnnotation = false; int value = 0; for (Annotation annotation : annotations) { if (annotation instanceof Retry) { hasRetryAnnotation = true; value = ((Retry) annotation).value(); } } final boolean shouldRetryCall = hasRetryAnnotation; final int maxRetries = value; final CallAdapter delegate = retrofit.nextCallAdapter(this, returnType, annotations); return new CallAdapter() { @Override public Type responseType() { return delegate.responseType(); } @Override public Object adapt(Call call) { return delegate.adapt(shouldRetryCall ? new RetryingCall<>(call, mExecutor, maxRetries) : call); } }; } static final class RetryingCall implements Call { private final Call mDelegate; private final ScheduledExecutorService mExecutor; private final int mMaxRetries; public RetryingCall(Call delegate, ScheduledExecutorService executor, int maxRetries) { mDelegate = delegate; mExecutor = executor; mMaxRetries = maxRetries; } @Override public Response execute() throws IOException { return mDelegate.execute(); } @Override public void enqueue(Callback callback) { mDelegate.enqueue(new RetryingCallback<>(mDelegate, callback, mExecutor, mMaxRetries)); } @Override public void cancel() { mDelegate.cancel(); } @SuppressWarnings("CloneDoesntCallSuperClone" /* Performing deep clone */) @Override public Call clone() { return new RetryingCall<>(mDelegate.clone(), mExecutor, mMaxRetries); } } // Exponential backoff approach from https://developers.google.com/drive/web/handle-errors static final class RetryingCallback implements Callback { private static Random random = new Random(); private final int mMaxRetries; private final Call mCall; private final Callback mDelegate; private final ScheduledExecutorService mExecutor; private final int mRetries; RetryingCallback(Call call, Callback delegate, ScheduledExecutorService executor, int maxRetries) { this(call, delegate, executor, maxRetries, 0); } RetryingCallback(Call call, Callback delegate, ScheduledExecutorService executor, int maxRetries, int retries) { mCall = call; mDelegate = delegate; mExecutor = executor; mMaxRetries = maxRetries; mRetries = retries; } @Override public void onResponse(Response response, Retrofit retrofit) { mDelegate.onResponse(response, retrofit); } @Override public void onFailure(Throwable throwable) { // Retry failed request if (mRetries < mMaxRetries) { retryCall(); } else { mDelegate.onFailure(new TimeoutError(throwable)); } } private void retryCall() { mExecutor.schedule(new Runnable() { @Override public void run() { final Call call = mCall.clone(); call.enqueue(new RetryingCallback<>(call, mDelegate, mExecutor, mMaxRetries, mRetries + 1)); } }, (1 << mRetries) * 1000 + random.nextInt(1001), TimeUnit.MILLISECONDS); } } }