Intelligent Technology's Technical Blog

株式会社インテリジェントテクノロジーの技術情報ブログです。

Android Volleyライブラリのリトライ回数を増やすと・・・

こんにちは、中山です。

Android用の通信ライブラリである「Volley」。
ウェブで検索すると、いろいろな解説記事が見つかります。

複雑になってしまいがちな通信関連処理が、簡単に実装できるようになるこの「Volley」ライブラリ。
もちろん、通信のタイムアウトや、通信失敗時のリトライ回数なども、簡単に設定することができます。

しかしこのリトライ回数を増やしたとき、ちょっと想像していなかった挙動になりましたので、紹介してみたいと思います。


まずはVolleyライブラリが動作するサンプルアプリケーションを作成します。
今回は、Android Studioから「MyVolleySample」という新規プロジェクトを作成しました。

「build.gradle」ファイルを開いて、以下のようにVolleyライブラリの設定を追加します。(dependenciesブロック内に「compile 'com.mcxiaoke.volley:library:1.0+'」を追加します。)

f:id:IntelligentTechnology:20150525161400p:plain:w520

※「com.mcxiaoke.volley」は、非公式のVolleyのミラーリポジトリではありますが、今回は手順簡略化のため、こちらを利用しています。
正式な導入方法はこちらなどの情報をご覧ください。

またアプリからのインターネット通信を可能とするため、AndroidManifest.xmlにパーミッションを追加します。

f:id:IntelligentTechnology:20150525161725p:plain:w480

Volleyライブラリの機能は、こちらの情報にあるとおり、Singletonクラスとして提供することとします。
今回のサンプルアプリでは、「MyVolleySingleton」というクラスを作成しました。

import android.content.Context;
import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.toolbox.Volley;

public class MyVolleySingleton {

    private final Context mCtx;
    private final RequestQueue mRequestQueue;
    private static MyVolleySingleton mInstance = null;

    private MyVolleySingleton(Context context) {
        mCtx = context;

        // getApplicationContext() is key, it keeps you from leaking the
        // Activity or BroadcastReceiver if someone passes one in.
        mRequestQueue = Volley.newRequestQueue(mCtx.getApplicationContext());
    }

    public static synchronized MyVolleySingleton getInstance(Context context) {
        if (mInstance == null) {
            mInstance = new MyVolleySingleton(context);
        }
        return mInstance;
    }

    public <T> void addToRequestQueue(Request<T> req) {
        mRequestQueue.add(req);
    }
}

また今回は、シンプルな、文字列としてレスポンスを受け取るためのStringRequestクラスを使って、リクエストを実行するようにします。

準備はだいたい整いつつあるのですが、肝心の、タイムアウトとリトライの設定は、いったいどこで行えばよいのでしょうか?
この答えは、今回利用するStringRequestクラスのスーパークラスである、「Request」クラスのコンストラクタ内にあります。

public Request(int method, String url, Response.ErrorListener listener) {
    mMethod = method;
    mUrl = url;
    mErrorListener = listener;
    setRetryPolicy(new DefaultRetryPolicy()); // タイムアウト、リトライの設定

    mDefaultTrafficStatsTag = findDefaultTrafficStatsTag(url);
}

上記のように、「setRetryPolicy」メソッドを呼び出して設定しているようです。
この例では、setRetryPolicyメソッドのパラメータに「DefaultRetryPolicy」クラスの、パラメータ無しコンストラクタを指定しています。これはつまり、タイムアウト、リトライ回数は、このDefaultRetryPolicyクラスに定義されている初期値をそのまま使う、ということになるようです。

DefaultRetryPolicyクラスの中身を見てみますと、それぞれの初期値は以下のように、

/** The default socket timeout in milliseconds */
public static final int DEFAULT_TIMEOUT_MS = 2500;
/** The default number of retries */
public static final int DEFAULT_MAX_RETRIES = 1;

タイムアウト初期値は2500ミリ秒(=2.5秒)、リトライ回数初期値は1回(=最初に通信失敗した後、1回だけリトライする)に設定されているようです。

これらの値を変更する場合は、次のようにパラメータ付きのDefaultRetryPolicyクラスのコンストラクタを用いて、

setRetryPolicy(new DefaultRetryPolicy(5000, 
                                      3, 
                                      DefaultRetryPolicy.DEFAULT_BACKOFF_MULT));

のようにします。
この例では、タイムアウトを5000ミリ秒(=5秒)、リトライ回数を3回に設定しています。(※コンストラクタの3番目のパラメータは、タイムアウト値に適用する係数です。今回は初期値のまま(=1)としています。)

今回は、(いろいろやりかたはあるとは思いますが)StringRequestクラスを継承した「MyStringRequest」クラスを新たに作成して、そのコンストラクタで、「setRetryPolicy」メソッドを呼び出し、タイムアウト、リトライ回数を設定するようにしました。

MyStringRequestクラスは以下のようになりました。

import com.android.volley.DefaultRetryPolicy;
import com.android.volley.Response;
import com.android.volley.toolbox.StringRequest;

public class MyStringRequest extends StringRequest {

    // タイムアウト値(5000ミリ秒)
    private static final int TIMEOUT_MS = 5000;
    // リトライ回数(初回リクエスト失敗後、3回リトライ)
    private static final int MAX_RETRIES = 3;

    public MyStringRequest(int method, 
                           String url, 
                           Response.Listener<String> listener, 
                           Response.ErrorListener errorListener) {
        super(method, url, listener, errorListener);

        // タイムアウト値、リトライ回数をリトライポリシーに設定する
        setRetryPolicy(new DefaultRetryPolicy(TIMEOUT_MS, 
                                              MAX_RETRIES, 
                                              DefaultRetryPolicy.DEFAULT_BACKOFF_MULT));
    }
}

ここまでの実装を踏まえまして、Volleyライブラリを用いた通信処理の呼び出しは、以下のようになります。

    @Override
    protected void onResume() {
        super.onResume();

        // リクエスト先のURL(エミュレータから見たホストマシンのローカルホストのPHPファイルを指定)
        String url ="http://10.0.2.2/delay.php";

        // リクエストオブジェクトのインスタンスを生成
        MyStringRequest stringRequest = new MyStringRequest(Request.Method.GET, url,
            new Response.Listener<String>() {
                @Override
                public void onResponse(String response) {
                    // 正常にリクエストが完了した場合、そのレスポンスをToast表示(先頭20文字のみ)
                    Toast.makeText(MainActivity.this, 
                                   "Response is: " + response.substring(0, 20), 
                                   Toast.LENGTH_SHORT).show();
                }
            }, new Response.ErrorListener() {
                @Override
                public void onErrorResponse(VolleyError error) {
                    // リクエストが失敗した場合
                    Log.i("MyVolleySample", "Failed.");
                    Toast.makeText(MainActivity.this, 
                                   "That didn't work!", 
                                   Toast.LENGTH_SHORT).show();
                }
            }
        );

        // リクエストキューにリクエストを追加(=リクエスト実行)
        MyVolleySingleton.getInstance(this).addToRequestQueue(stringRequest);
    }

リクエスト先のサーバモジュール(PHPモジュール)では、タイムアウトやリトライを試せるように、レスポンスを返すまでに60秒かかるように設定しました。
この状態でリクエストを実行したとき、サーバ側のアクセスログには以下のように出力されていました。

127.0.0.1 - - [25/May/2015:17:13:10 +0900] "GET /delay.php HTTP/1.1" 200 41
127.0.0.1 - - [25/May/2015:17:13:15 +0900] "GET /delay.php HTTP/1.1" 200 41
127.0.0.1 - - [25/May/2015:17:13:25 +0900] "GET /delay.php HTTP/1.1" 200 41
127.0.0.1 - - [25/May/2015:17:13:45 +0900] "GET /delay.php HTTP/1.1" 200 41

また、アプリ側のLogCatには以下のような出力がありました。

05-25 17:14:25.811: I/MyVolleySample(2475): Failed.

ここでちょっとおもしろい結果が見られました。処理の流れを時間ごとに追ってみますと、

13分10秒 初回リクエスト。
 (5秒後)
13分15秒 初回リクエストタイムアウト。1回目のリトライ。
 (10秒後)
13分25秒 1回目のリトライリクエストタイムアウト。2回目のリトライ。
 (20秒後)
13分45秒 2回目のリトライリクエストタイムアウト。3回目のリトライ。
 (40秒後)
14分25秒 3回目のリトライリクエストタイムアウト。通信失敗がアプリに通知される。

このように、リトライのたびに、タイムアウト間隔が倍、倍になっていっているのです。

これは、VolleyライブラリのDefaultRetryPolicyクラスの「retry」メソッドの中で、リトライが実施されるたびに、タイムアウト値を2倍しているためのようです。
※正確には、タイムアウト値(=mCurrentTimeoutMsフィールド)に係数(=mBackoffMultiplierフィールド。この場合、値は1です)を掛けたものを元のタイムアウト値に加算しているようです。

public void retry(VolleyError error) throws VolleyError {
    mCurrentRetryCount++;
    mCurrentTimeoutMs += (mCurrentTimeoutMs * mBackoffMultiplier);
    if (!hasAttemptRemaining()) {
        throw error;
    }
}

したがいまして、リトライ回数を多く設定したときに、一緒にタイムアウト値も大きく設定してしまうと、どんどんタイムアウト時間が長くなってしまい、いつまでたってもレスポンスが返ってこない、ということにもなりかねません。この点は、少し注意が必要かもしれませんね。

以上、便利なVolleyライブラリの、知っておくとちょっと役に立つ情報でした。