本日ご紹介するのは、職場で契約して利用しているBacklogのユーザーやプロジェクトの棚卸しの業務の中で遭遇した問題とその対処法です。
Problem
実は結構以前、今年の1月にBacklog APIについて単位時間あたりのAPI要求数の上限値が設定されました。
制限はユーザーの単位で、1分あたりの要求数が一定値に達するとHTTPステータスコード429を返すようになる仕様です。アクセス元が異なったりAPIキーが異なったりしても、ユーザーが同じならまとめてカウントされるそうです。
コード429制限は60秒経過後に解除されます。
詳細はこちらのニュースで案内されています。
前述の「棚卸し」作業では、過去プロジェクトのWikiや課題の情報をごっそりエクスポートするアプリケーションを実行する必要があり、このときAPI要求数の上限に達して問題に気がついたわけです。
Solution
Backlog APIの公式のJava言語向けクライアント・ライブラリであるBacklog4Jは、今のところ、自動リトライの仕組みを持っておらず、「次にアクセスOKとなるまでの時間はこれくらい」という情報の提供しかしてくれないようです。。
というわけで、当座は、クライアントを利用するアプリケーション毎、それもクライアントを使ってAPIに要求をする箇所毎に対処する必要があるわけです。
BacklogClient を使用してAPIにリクエストを送信した時 BacklogAPIException
がスローされたら、その getStatusCode()
メソッドで得られる数値をチェックして、 429
なら少し時間をいてから再実行する。こういうロジックを埋め込みます。
面倒なことこの上ないですがしかたがありません・・・。
ご参考まで、今回は以下のようなヘルパー関数を書いて乗り切りました(Java8以前であれば再代入や条件分岐をたくさん含んだ気持ちの悪いコードになっていたことでしょう):
public static void main(string[] args) {
final BacklogClient client = /* ...クライアント初期化コード... */;
final Project project = tryRequest(() -> client.getProject("GISUI"));
/* ...中略... */
}
private static <R> R tryRequest(Supplier<R> codeCallsApi) {
// トライの最大回数
final int maxTryCount = 3;
// 最大トライ回数に到達した場合にスローする例外を生成する関数
final Supplier<RuntimeException> exception = () ->
new RuntimeException("try count has been exceeded.");
// APIリクエストを試みる関数
// 戻り値は3種類:
// (A) APIから結果が得られた場合 Some(Some(R))
// (B) APIからコード429が返された場合 Some(None)
// (C) APIからコード429以外が返された場合 None
final IntFunction<Optional<Optional<R>>> doRequest = n -> {
// 2回目以降の場合はトライ前にインターバルを置く
if (0 < n) {
System.out.println("pause for Backlog API usage limit.");
trySleep(61 * 1000);
System.out.println("resume. try count = " + n);
}
try {
// Backlog APIへのリクエストを行う => (A)
return Optional.of(Optional.of(codeCallsApi.get()));
} catch (BacklogAPIException e) {
System.err.println("error: " + e.getMessage());
// ステータスコードをチェック
// ステータスコードが429(単位時間あたりの要求最大数に到達)の場合、リトライを促す => (B)
// ステータスコードが429以外の場合、リトライでは解決しないので中断を促す => (C)
return e.getStatusCode() == 429 ? Optional.of(Optional.empty()) : Optional.empty();
}
};
// トライして結果を得るためのストリームを生成
// 有効な結果があればそれを返し、なければ例外をスローする
// Streamは遅延評価されるので、ひとたびAPIから有効な値が得られたらその後のAPI要求は発生しない
return IntStream.range(0, maxTryCount) // 0, 1, 2, ...
.mapToObj(doRequest) // API要求トライ
.takeWhile(Optional::isPresent) // Some(Some(R))ないしSome(None) の間だけ繰り返す
.map(Optional::get) // Some(...) を Some(R) ないし None に変換
.filter(Optional::isPresent) // Some(R) のものだけ取り出す
.map(Optional::get) // Some(R) を R に変換
.findFirst().orElseThrow(exception); // 最初の要素を取り出す
}