Dispatch a recursive handler from IntentService to retry the http call
Every time my request fails, I try to implement exponential backoff to retry the failed http call by scheduling a thread with handler.postDelayed(...
). The problem is that I’m doing this from IntentService, which dies after scheduling the first thread, so the handler can’t call itself. I get the following error:
java.lang.IllegalStateException: Handler (android.os.Handler) {2f31b19b} sending message to a Handler on a dead thread
My IntentService class:
@Override
protected void onHandleIntent(Intent intent) {
......
Handler handler = new Handler();
HttpRunnable httpRunnable = new HttpRunnable(info, handler);
handler.postDelayed(httpRunnable, 0);
}
My custom Runnable:
public class HttpRunnable implements Runnable {
private String info;
private static final String TAG = "HttpRunnable";
Handler handler = null;
int maxTries = 10;
int retryCount = 0;
int retryDelay = 1000; Set the first delay here which will increase exponentially with each retry
public HttpRunnable(String info, Handler handler) {
this.info = info;
this.handler = handler;
}
@Override
public void run() {
try {
Call my class which takes care of the http call
ApiBridge.getInstance().makeHttpCall(info);
} catch (Exception e) {
Log.d(TAG, e.toString());
if (maxTries > retryCount) {
Log.d(TAG,"%nRetrying in " + retryDelay / 1000 + " seconds");
retryCount++;
handler.postDelayed(this, retryDelay);
retryDelay = retryDelay * 2;
}
}
}
}
Is there a way to keep my handler in the Activity state? What is the best/cleanest way to schedule my HTTP retries using exponential backoff?
Solution
The main advantage of using IntentService
is that it handles all background threads for you in the onHandleIntent(Intent intent)
method. In this case, you have no reason to manage the handler yourself.
You can use AlarmManager
to schedule an intent to be sent to your service. You will keep the retry information in the intent to be delivered.
I thought so:
public class YourService extends IntentService {
private static final String EXTRA_FAILED_ATTEMPTS = "com.your.package.EXTRA_FAILED_ATTEMPTS";
private static final String EXTRA_LAST_DELAY = "com.your.package.EXTRA_LAST_DELAY";
private static final int MAX_RETRIES = 10;
private static final int RETRY_DELAY = 1000;
public YourService() {
super("YourService");
}
@Override
protected final void onHandleIntent(Intent intent) {
Your other code obtaining your info string.
try {
Make your http call.
ApiBridge.getInstance().makeHttpCall(info);
} catch (Exception e) {
Get the number of previously failed attempts, and add one.
int failedAttempts = intent.getIntExtra(EXTRA_FAILED_ATTEMPTS, 0) + 1;
if we have failed less than the max retries, reschedule the intent
if (failedAttempts < MAX_RETRIES) {
calculate the next delay
int lastDelay = intent.getIntExtra(EXTRA_LAST_DELAY, 0);
int thisDelay;
if (lastDelay == 0) {
thisDelay = RETRY_DELAY;
} else {
thisDelay = lastDelay * 2;
}
update the intent with the latest retry info
intent.putExtra(EXTRA_FAILED_ATTEMPTS, failedAttempts);
intent.putExtra(EXTRA_LAST_DELAY, thisDelay);
get the alarm manager
AlarmManager alarmManager = (AlarmManager) getSystemService(ALARM_SERVICE);
make the pending intent
PendingIntent pendingIntent = PendingIntent
.getService(getApplicationContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
schedule the intent for future delivery
alarmManager.set(AlarmManager.RTC_WAKEUP,
System.currentTimeMillis() + thisDelay, pendingIntent);
}
}
}
}
This simply tells the IntentService you’re using to process the call in the background and then schedule the intent to be resent each time it fails, adding extra information to it like the number of retries and the length of the retry last time the delay was.
Note: If you attempt to send multiple intents to this service, but more than one fails and must be rescheduled using AlarmManager
, only the latest intents will be delivered to Intent.filterEquals(Intent intent) if the intents
are considered equal. This will be a problem if your intent is the same except for the extra content attached to it, and you must use a unique requestCode for each intent you want to reschedule when you create your PendingIntent
. Something along these lines:
int requestCode = getNextRequestCode();
PendingIntent pendingIntent = PendingIntent
.getService(getApplicationContext(), requestCode, intent, 0);
I guess you can use your shared preferences to store a request code that is incremented every time you have to schedule a retry.