3

I am trying to implement exponential backoff for retrying failed http calls by scheduling a thread with handler.postDelayed(...) every time my request fail. The problem is that I am doing this from an IntentService which dies after scheduling the first thread so the handler is not able to call itself. I am getting the following error:

java.lang.IllegalStateException: Handler (android.os.Handler) {2f31b19b} sending message to a Handler on a dead thread

My class with the IntentService:

@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 alive? What would be the best/cleanest way for scheduling my http retries with an exponential backoff?

Crocodile
  • 5,724
  • 11
  • 41
  • 67

1 Answers1

4

The main advantage to using an IntentService is that it handles all the background threading for you inside of it's onHandleIntent(Intent intent) method. There is no reason for you to manage a handler yourself in this case.

Here's a way you could approach this using the AlarmManager to schedule the delivery of an intent to your service. You would keep the retry information in the intent to be delivered.

I'm thinking something like this:

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 lets the IntentService you are using handle doing the call in the background, and then schedules the intent to be re-sent each time it fails, adding extras to it with how many times it has been retried and how long the last retry delay was.

Note: If you are trying to send multiple intents to this service and more than one fails and must be rescheduled with the AlarmManager, only the latest intent will be delivered if the intents are considered equal according to Intent.filterEquals(Intent intent). If your intents are identical with the exception of the extras attached to it, this will be a problem, and you must use a unique requestCode for each intent being rescheduled when creating your PendingIntent. Something along these lines:

int requestCode = getNextRequestCode();
PendingIntent pendingIntent = PendingIntent
    .getService(getApplicationContext(), requestCode, intent, 0);

I suppose you could use your shared preferences to store a request code that increments each time you have to schedule a retry.

Crocodile
  • 5,724
  • 11
  • 41
  • 67
Jazzer
  • 2,993
  • 1
  • 19
  • 24
  • 1
    Very nice and logical, thank you! I had to make a small fix by changing the last parameter of getService to 'PendingIntent.FLAG_UPDATE_CURRENT' – Crocodile Jun 04 '15 at 23:19