Kotlinのsuspend関数もかっこよくリトライしたい

# はじめに
少し柔軟に設定・利用できるように下記要件のリトライが仕込めるライブラリや実装をさがしていました
- コルーチンスコープ内でhttpリクエストをして、指定の方法でリトライをかけたい。
- HTTPリクエストを、リトライの設定もいれてメソッドインジェクション的に設定したい。

Springを使っていれば `RetryTemplate` が使えるのでそのまま使えて便利だけど、スレッドをブロックしてしまうのでコルーチンスコープ内で使うのは厳しいことがわかりました。

suspend関数のリトライ実装やライブラリを軽く調べるも、これしかヒットしない...
- [android - How to Exponential Backoff retry on kotlin coroutines - Stack Overflow](https://stackoverflow.com/questions/46872242/how-to-exponential-backoff-retry-on-kotlin-coroutines)
もうこれはちょうどいいクラスを自分で自作するしかなさそうか...？


また、コードでいうとこんな感じのイメージです:
```kotlin
this.httpClient
	.uri("https://endpoint.com")
	.setRetry(AsyncBackOffRetry())  // 設定と実行をわけたい
	.postAsync(request)  // ここで設定を使って実行
```

Stack Overflowの回答は秀逸で、そのままコピペでもリリトライができるけど、他のクラスと組み合わせるには少し工夫がいる。そこで、クラスベースに改良してみます。

## 改良案
Stack Overflowの回答にあった、メソッドの定義を参考に `AsyncBackoffRetry` クラスにラップする。
```kotlin
/**
 * リトライ設定クラス
 */
class AsyncBackoffRetry(
    /** 最大試行回数 */
    private val maxAttempts: Int,
    /**  初期遅延時間 */
    private val initialDelay: Long,
    /**  最大遅延時間 */
    private val maxDelay: Long,
    /** リトライ乗数 */
    private val multiplier: Double
) {
    suspend fun <T> doWithRetry(producer: suspend () -> T): T {
        var currentDelay = initialDelay
        // ログに出すように数えておくだけ
        var count = 1
        repeat(maxAttempts - 1) {
            count += it
            try {
                return producer()
            } catch (e: Exception) {
            	// ロギングなど好きな処理
            }
            delay(currentDelay)
            // 2回め以降は現在の遅延時間 * 乗数でどんどん待つ時間を長くしていく
            currentDelay = (currentDelay * multiplier).toLong().coerceAtMost(maxDelay)
        }
        println("再試行中 ... ${(count + 1)}回目")
        return producer()
    }
}
```

コンストラクタに入っているプロパティ系はほぼそのままお借りしました。元ネタは本当にいい感じでした...メソッドシグネチャ `suspend fun <T> doWithRetry(producer: suspend () -> T): T` とその実装部分はほぼそのまま。リトライの設定だけコンストラクタにしておきます。

これにより、リトライの設定と任意のリトライしたい処理を分離できるようになりました。


## 使い方
手動でリトライをしたいのであれば、たんにインスタンスを生成すればOK。これじゃクラスにした意味があんまない気もするけど...
```kotlin
val retry = AsyncBackoffRetry(
    maxAttempts = 3,
    maxDelay = 4000,
    initialDelay = 2000,
    multiplier = 2.0
)

val result = retry.doWithRetry {
    // なんでも好きなsuspend関数
    service.execute()
}
```

クラスにしたことで外から依存の注入ができるようになる。冒頭のHTTPコールの例であれば、HTTPクライアントにメソッドインジェクションの形でリトライの設定を仕込める。

`AsyncBackoffRetry` を受け取るよう、 `setRetry` メソッドをはやしておく。
```kotlin
interface HttpClient {
    fun setUri(uri: String): HttpClient
	setRetry(retry: AsyncBackoffRetry): HttpClient
	suspend fun <T> postAsync(httpRequestBody: T): String
}

// イメージコードなのでコンパイルは通らないです...
class DefaultHttpClient : HttpClient {
    private val httpClient = HttpClient.newBuilder()
        .version(Version.HTTP_1_1)
        .build()
    private var retry: AsyncBackoffRetry? = null
    private var uri: String = ""
    
    override fun setUri(uri: String): HttpClient = apply {
        this.uri = uri
    }

    override fun setRetry(retry: AsyncBackoffRetry): HttpClient = apply {
        this.retry = retry
    }

    override suspend fun <T> postAsync(httpRequestBody: T): String = apply {
        // httpRequest は別途初期化しておく ...
        if (retry != null) {
            // クラスに入っているRetryの設定を使って処理をリトライ仕様にできる
            return retry.doWithRetry {
                httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofString())
                    .await()
                    .body()
            }
        }

        return httpClient
            .sendAsync(httpRequest, HttpResponse.BodyHandlers.ofString())
            .await()
            .body()
    }
}
```

リトライを設定に組み込んで、冒頭のHTTPリクエストをやってみると
```kotlin
val response =DefaultHttpClient()
      .setUri("https://endpoint.com")
      .setRetry(AsyncBackoffRetry(
          maxAttempts = 3,
          maxDelay = 4000,
          initialDelay = 2000,
          multiplier = 2.0
      ))
      .postAsync(body)
```

処理に失敗すると試行都度処理を実行した旨が出力されます。
```
再試行中 ... 1回目
再試行中 ... 2回目
```

設定と実装をきれいにわけられているし、HTTPリクエストのパーツとリトライ設定のパーツもうまく責務を分離できており、便利になりました。

# おまけ
## 抽象化、汎化
リトライの仕方を局所的に変えたい、みたいな要件があるのであればインターフェースに抽出して、引数をインターフェースに変更すればDIライクな実装にもできます。
```kotlin
interface AsyncRetry {
    suspend fun <T> doWithRetry(producer: suspend () -> T): T
}
```
さきほどの `AsyncBackoffRetry` にインターフェースを実装させつつ `AsyncSimpleRetry` といった別のリトライ戦略をもったクラスも用意できるようになります。
自分のプロジェクトでは要件になかったので不要でしたが、共通ライブラリ化するのであればこっちのほうが明らかにいいです。

また、もう少し本格的にやるのであればリトライは設定と実行の2パートなのでStrategyパターンも有効そうです。
さきほどの `AsyncBackoffRetry` にインターフェースを実装させつつ `AsyncSimpleRetry` といった別のリトライ戦略をもったクラスも用意できるようになります。
spring-retry の [`RetryTemplate`](https://github.com/spring-projects/spring-retry/blob/main/src/main/java/org/springframework/retry/support/RetryTemplate.java)はStrategyパターンでポリシーと実行に分けて使えるようになっています。


## KotlinにJavaの並行処理を組み合わせる
suspend関数はJavaの `CompletableFuture` と互換になります。この互換性は [kotlinx-coroutines-jdk8](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-jdk8/index.html) により追加でき、JavaのメソッドでCompletableFutureを返す場合、suspend関数内でコルーチンスコープの関数と同等に扱えます（雰囲気的には、jsでPromiseを返す関数をasyncでマークするような感じです）。

この記事で扱った、コルーチンスコープ内のHTTPリクエストにはJava11で追加されたjava.netパッケージのHTTPクライアントを使用しており、 [`sendAsync`](https://docs.oracle.com/javase/jp/11/docs/api/java.net.http/java/net/http/HttpClient.html#sendAsync(java.net.http.HttpRequest,java.net.http.HttpResponse.BodyHandler)) メソッドがCompletableFutureを返すためコルーチンと組み合わせることができる、ということでした。


これくらいのクラスなら自分でも書いていいな、と思って用意したもののいい感じのライブラリないんですかね... `CompletableFuture` をラップしてリトライできるライブラリがあればkotlinx-coroutinesと組み合わせてリトライもできるのかも？