Promise をリトライする何かを作った

[cloudpack あら便利カレンダー 2017](https://cloudpack.github.io/benri2017/) 2日目の記事です。

# tl;dr

タイトルの通りですが、 [retryx](https://github.com/y13i/retryx) というライブラリーを作りました。

# 経緯と動機

1. API Gateway + Lambda を使ったシステムで、あるリクエストで発火する function A が30秒（API Gateway のタイムアウト秒数）でレスポンスを返せず、困る。
1. A を A' と A'' に分割して、 A' は A'' を invoke して即座に API Gateway にレスポンスを返す、 A'' は時間がかかる処理をする、という非同期実行に変更。
1. それだけだとフロントエンドから長い時間がかかる処理の結果が受け取れないので、 function B を作り A'' の処理結果が S3 に置かれている時だけ 200 OK を返すようにしよう。
1. フロントエンドは　B をハンドラーにした API Gateway のパスをポーリングして、 OK でなければ一定時間待機して再度リクエストするようにすればよくね？
1. フロントエンドの処理がめんどい（楽に書きたい）

多分似たようなライブラリーは少なからずあると思うけど、せっかく書いたこの処理をもうちょい膨らませてライブラリー化してみたかった。

# インストール

```shell-session
$ npm install --save retryx
```

# 解説

フローを図示するとこんな感じです。

![diagram.png](https://qiita-image-store.s3.amazonaws.com/0/78376/b3bc1d98-5026-2bde-a789-96c6e510f0e7.png)

`retryx()` はリトライを含んだ全体の処理の `Promise` を返します。

`retryx()` の第一引数である `main` はラップしたい（失敗したらリトライさせたい）処理本体です。 `Promise` ではなく、 `Promise` を返す関数を渡す点に注意。

`maxTries` は読んで字の如く最大試行回数です。デフォルトは5回です。

`waiter` は再試行前に待機するための関数です。ここも `Promise` で実装されており、デフォルトではいわゆる _exponential backoff_ に従って試行回数に応じて指数関数的に増える待機時間を待って解決する `Promise` を返す関数がセットされています。デフォルトでは 100ms → 400ms → 900ms → 1600ms と変化します。5回失敗すると処理自体の時間を除けばちょうど3秒です。

- [AWSユーザーは必ず覚えておきたいExponential Backoffアルゴリズムとは何か - yoshidashingo](http://yoshidashingo.hatenablog.com/entry/2014/08/17/135017)

毎回同じ時間待機したい、待機時間にランダム性を加えたい、指数関数的に待機時間を増やすがある程度のところで上限を設けたい、などの要件がある場合はこのオプションにそのように実装した関数を渡せば大丈夫です。

`retryCondition` は試行回数に関わらず再試行を行うかどうかの判定を行う関数です。デフォルトでは常に `true` を返すようになっています。

もし `main` が失敗し、 `onRejected()` に渡されるエラーがリトライでは解決不可能だったりした場合に、この関数で `false` を返すと試行回数が最大試行回数に達していなくても `retryx()` が失敗されます。

`beforeTry`, `afterTry`, `beforeWait`, `afterWait`, `doFinally` はデフォルトでは何もしません。もし詳細にログを出力したい、成功しても失敗しても必ず最後に行いたい処理がある、などがあれば使えるかなあと思っています。

# 使用例

[AWS SDK](https://github.com/aws/aws-sdk-js) と組み合わせてみた例。 AWS API を叩きまくる人は一度はスロットリングに泣かされたはず。

```js
const retryx = require("retryx");
const AWS = require("aws-sdk");

const ec2 = new AWS.EC2();

retryx(() => ec2.describeRegions().promise()).then(response => {
  console.log(response);
});
```

[axios](https://github.com/mzabriskie/axios) と組み合わせてみた例。

```js
const retryx = require("retryx");
const axios = require("axios");

retryx(() => axios.get("http://example.com")).then(response => {
  console.log(response.statusText);
});
```

もちろん `async/await` も使えます。

```js
import retryx from "retryx";

(async () => {
  try {
    const result = await retryx(() => {
      const number = Math.round(Math.random() * 100);

      if (number > 95) {
        return number;
      } else {
        throw number;
      }
    });

    console.log("success", result);
  } catch (n) {
    console.log("fail:", n)
  }
})();
```

待機する前にそのことを表示してみる例。ついでに待機時間のアルゴリズムも単純に100ms待つものに。

```javascript
const retryx = require("retryx");

retryx(() => {
  const number = Math.round(Math.random() * 100);
  return number > 95 ? Promise.resolve(number) : Promise.reject(number);
}, {
  maxTries:   100,
  beforeWait: (tries) => console.log(`try #${tries} failed. wait 100 ms`),
  waiter:     () => new Promise((r) => setTimeout(r, 100)),
}).then(result => {
  console.log(`success: ${result}`);
});
```

TypeScript の型推論が効きます。 [VSCode](https://code.visualstudio.com/) 最高！ 

```typescript
import retryx from "retryx";

(async () => {
  let result = await retryx(() => 123);
  result = "abc"; // ERROR: Type '"abc"' is not assignable to type 'number'.
  // `retryx(() => 123)` は `Promise<number>` で、 `await` すると `number` になります
})();
```

TypeScript のジェネリクスで、最終的に Promise が解決する値の型を明示的に指定できます。

```typescript
import retryx from "retryx";

(async () => {
  let result = await retryx<string>(() => { // 明示的に `string` であると指定
    const number = Math.round(Math.random() * 100);

    if (number < 50) {
      throw new Error();
    } else if (number < 80) {
      return "good";
    } else if (number < 90) {
      return "great";
    } else {
      return number; // ERROR! main Promise が `number` に解決することは上での定義からあり得ないのでコンパイルエラーになる
    }
  });
})();
```

# さいごに

車輪の再発明楽しい。