Nuxt.js + JavaScriptでエクスポネンシャルバックオフを実装する

## エクスポネンシャルバックオフとは？

アルゴリズムの一つです。  
直訳すると「指数関数的後退」で、いわゆるリトライ処理の間隔を指数関数的に増加させる方式のことです。

リトライ間隔例：1秒、2秒、4秒、8秒・・・



[AWS でのエラー再試行とエクスポネンシャルバックオフ](https://docs.aws.amazon.com/ja_jp/general/latest/gr/api-retries.html)



## ユースケース

主にネットワークエラーに対しての再試行の方式として使用します。  
一時的な負荷のエラーに関してはリトライで成功する可能性が高いですが、一般的な「ｎ秒後にリトライ」を実施する方式だとｎ秒ごとに同じ量の負荷がかかり続けることが懸念されます。  
エクスポネンシャルバックオフ方式を使用して指数関数的にリトライ間隔を増加させることで、効率的に負荷分散を行えます。

## やること

- リトライボタンを押すたびに指数関数的にリトライ処理までの間隔を増加させる

## 本題

Nuxt.js + Vuetifyを使用して作っていきます。

### プロジェクト作成

yarn create nuxt-appでサクッと作成します。

[Nuxt.js インストール](https://ja.nuxtjs.org/guide/installation/)

```sh
yarn create nuxt-app exponential-backoff-sample
yarn create v1.22.4
[1/4] Resolving packages...
warning create-nuxt-app > sao > micromatch > snapdragon > source-map-resolve > resolve-url@0.2.1: https://github.com/lydell/resolve-url#deprecated
warning create-nuxt-app > sao > micromatch > snapdragon > source-map-resolve > urix@0.1.0: Please see https://github.com/lydell/urix#deprecated
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Installed "create-nuxt-app@2.15.0" with binaries:
      - create-nuxt-app

create-nuxt-app v2.15.0
✨  Generating Nuxt.js project in exponential-backoff-sample
? Project name exponential-backoff-sample
? Project description My praiseworthy Nuxt.js project
? Author name author
? Choose programming language JavaScript     
? Choose the package manager Yarn     
? Choose UI framework Vuetify.js
? Choose custom server framework None (Recommended)
? Choose Nuxt.js modules (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Choose linting tools (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Choose test framework None
? Choose rendering mode Universal (SSR)
? Choose development tools (Press <space> to select, <a> to toggle all, <i> to invert selection)

�  Successfully created project exponential-backoff-sample

  To get started:

        cd exponential-backoff-sample
        yarn dev

  To build & start for production:

        cd exponential-backoff-sample
        yarn build
        yarn start
```

### 実装

pages/index.vue

```vue

<template>
  <v-layout column justify-center align-center>
    <v-flex xs12 sm8 md6>
      <div class="text-center">
        <logo />
        <vuetify-logo />
      </div>
      <v-card>
        <v-card-title class="headline">
          exponential-backoff-sample
        </v-card-title>
        <v-card-text>
          正：{{ judgeBaseNum }}<br />
          ランダム数字{{ judgeNum }}<br /><br />
          最大リトライ回数：{{ maxRetryNum }}<br />
          リトライ回数：{{ retryNum }}<br />
          リトライ間隔：{{ retryInterval }}
        </v-card-text>
        <v-card-actions>
          <v-spacer />
          <v-btn color="primary" @click="retry">
            Retry
          </v-btn>
        </v-card-actions>
      </v-card>
    </v-flex>
  </v-layout>
</template>
<script>
import Logo from "~/components/Logo.vue";
import VuetifyLogo from "~/components/VuetifyLogo.vue";

export default {
  data() {
    return {
      retryNum: 0,
      retryInterval: 100,
      judgeBaseNum: 1,
      judgeNum: 0,
      maxRetryNum: 5
    };
  },
  components: {
    Logo,
    VuetifyLogo
  },
  methods: {
    // メイン処理
    async retry() {
      // 進捗リセット
      this.retryNum = 0;
      this.retryInterval = 100;
      while (this.retryNum < this.maxRetryNum) {
        this.retryNum++;
        this.judge();
        if (this.judgeNum === this.judgeBaseNum) {
          break;
        }
        // スリープ処理
        this.retryInterval = this.retryNum * 100 + this.retryInterval;
        await this.sleep(this.retryInterval);
      }
    },
    // ランダム数字生成
    judge() {
      const random = Math.ceil(Math.random() * 6);
      this.judgeNum = random;
    },
    // スリープ
    sleep(milliseconds) {
      return new Promise(resolve => setTimeout(resolve, milliseconds));
    }
  }
};
</script>
```

### リトライ処理

重要なのはこの部分です。

```javascript
// スリープ処理
this.retryInterval = this.retryNum * 100 + this.retryInterval;
await this.sleep(this.retryInterval);
```

リトライ間隔をリトライ回数  * リトライ間隔基準値を現時点のリトライ間隔に足しています。  
この部分がエクスポネンシャルバックオフと呼ばれている方式です。

補足ですが、スリープ処理は以下を参考にしました。

[JavaScriptでループ中にスリープしたい。それも読みやすいコードで - Qiita](https://qiita.com/akyao/items/a718cc78436df68d7e15#comment-9a57c092514237fbf558)

## 動作確認

動作確認してみましょう。

```sh
yarn dev
yarn run v1.22.4
$ nuxt

   ╭─────────────────────────────────────────────╮
   │                                             │
   │   Nuxt.js v2.12.2                           │
   │   Running in development mode (universal)   │
   │                                             │
   │   Listening on: http://localhost:3000/      │
   │                                             │
   ╰─────────────────────────────────────────────╯

i Preparing project for development
i Initial build may take a while
√ Builder initialized
√ Nuxt files generated

√ Client
  Compiled successfully in 10.40s

√ Server
  Compiled successfully in 9.19s

i Waiting for file changes
i Memory usage: 348 MB (RSS: 443 MB)
i Listening on: http://localhost:3000/
```

[http://localhost:3000/](http://localhost:3000/)にアクセスします。

![Untitle2d.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246580/ce086d6b-fb75-572b-da63-d30a52b0379b.png)

RETRYボタンをクリックしてみます。  
ランダム数字の表示が切り替わり、ランダム数字が１になるかリトライ回数が５回になるまでリトライ処理が続きます。  
そのとき、リトライ間隔の表示が増加することと、実際にリトライされるまでの間隔が増加することが確認できます。

## おわりに

JavaScriptでエクスポネンシャルバックオフ方式を実装してみました。  
AWSはもちろん、様々なAPIコールなどで汎用的に必要な概念となります。  
いろいろな実現方式を自分の中で確立しておきたいですね。