Last active
June 13, 2023 02:50
-
-
Save imminent/ac7f2a222e22f4009af4 to your computer and use it in GitHub Desktop.
Call retrying with Retrofit 2.0
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.example.api; | |
import java.util.Map; | |
import retrofit.Call; | |
import retrofit.http.Body; | |
import retrofit.http.GET; | |
import retrofit.http.POST; | |
public interface Api { | |
@Retry(1) | |
@GET("action") | |
Call getAction(); | |
@Retry | |
@POST("action") | |
Call postAction(@Body Map<String, Object> requestBody); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.example.api; | |
import com.squareup.okhttp.OkHttpClient; | |
import retrofit.GsonConverterFactory; | |
import retrofit.Retrofit; | |
public class ApiClient { | |
public final Api api; | |
public ApiClient(OkHttpClient client) { | |
final Retrofit retrofit = new Retrofit.Builder() | |
.addCallAdapterFactory(RetryCallAdapterFactory.create()) | |
.baseUrl("https://api.example.com") | |
.build(); | |
api = retrofit.create(Api.class); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.example.api; | |
import java.lang.annotation.Documented; | |
import java.lang.annotation.Retention; | |
import java.lang.annotation.Target; | |
import static java.lang.annotation.ElementType.METHOD; | |
import static java.lang.annotation.RetentionPolicy.RUNTIME; | |
/** | |
* Makes the Call retry on failure | |
*/ | |
@Documented | |
@Target(METHOD) | |
@Retention(RUNTIME) | |
public @interface Retry { | |
int value() default 3; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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<Object>() { | |
@Override | |
public Type responseType() { | |
return delegate.responseType(); | |
} | |
@Override | |
public <R> Object adapt(Call<R> call) { | |
return delegate.adapt(shouldRetryCall ? new RetryingCall<>(call, mExecutor, maxRetries) : call); | |
} | |
}; | |
} | |
static final class RetryingCall<T> implements Call<T> { | |
private final Call<T> mDelegate; | |
private final ScheduledExecutorService mExecutor; | |
private final int mMaxRetries; | |
public RetryingCall(Call<T> delegate, ScheduledExecutorService executor, int maxRetries) { | |
mDelegate = delegate; | |
mExecutor = executor; | |
mMaxRetries = maxRetries; | |
} | |
@Override | |
public Response<T> execute() throws IOException { | |
return mDelegate.execute(); | |
} | |
@Override | |
public void enqueue(Callback<T> callback) { | |
mDelegate.enqueue(new RetryingCallback<>(mDelegate, callback, mExecutor, mMaxRetries)); | |
} | |
@Override | |
public void cancel() { | |
mDelegate.cancel(); | |
} | |
@SuppressWarnings("CloneDoesntCallSuperClone" /* Performing deep clone */) | |
@Override | |
public Call<T> clone() { | |
return new RetryingCall<>(mDelegate.clone(), mExecutor, mMaxRetries); | |
} | |
} | |
// Exponential backoff approach from https://developers.google.com/drive/web/handle-errors | |
static final class RetryingCallback<T> implements Callback<T> { | |
private static Random random = new Random(); | |
private final int mMaxRetries; | |
private final Call<T> mCall; | |
private final Callback<T> mDelegate; | |
private final ScheduledExecutorService mExecutor; | |
private final int mRetries; | |
RetryingCallback(Call<T> call, Callback<T> delegate, ScheduledExecutorService executor, int maxRetries) { | |
this(call, delegate, executor, maxRetries, 0); | |
} | |
RetryingCallback(Call<T> call, Callback<T> delegate, ScheduledExecutorService executor, int maxRetries, int retries) { | |
mCall = call; | |
mDelegate = delegate; | |
mExecutor = executor; | |
mMaxRetries = maxRetries; | |
mRetries = retries; | |
} | |
@Override | |
public void onResponse(Response<T> 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<T> call = mCall.clone(); | |
call.enqueue(new RetryingCallback<>(call, mDelegate, mExecutor, mMaxRetries, mRetries + 1)); | |
} | |
}, (1 << mRetries) * 1000 + random.nextInt(1001), TimeUnit.MILLISECONDS); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.example.api; | |
import java.io.IOException; | |
public class TimeoutError extends IOException { | |
private static final long serialVersionUID = -6469766654369165864L; | |
public TimeoutError() { | |
super(); | |
} | |
public TimeoutError(Throwable cause) { | |
super(cause); | |
} | |
} |
Thanks for the helper! What if we do sync calls with execute()
, how retry should be handled?
The above code is feasible With RETROFIT 1.x.
How it can be reused with RETROFIT 2. Since I am getting compile error while implementing this with Retrofit2. Please help. Thanks!
@developer100rbh, I'm not sure if you already solved, but I was needed to make something with retry and Retrofit 2, so there it is:
https://gist.github.com/dtodt/2b62a18e87375682167027bb7feb6752
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thanks for creating such a useful Retrofit retry helper!