node-twitterでstreamAPIに再接続するときの実装

[ブログ](http://www.ocknamo.com/entry/twitterのsteramapiに再接続するときの実装/)にも書いたものの転載です。

[node-twitter](https://www.npmjs.com/package/twitter)を使用するとtweetはもちろんfilter/streamの取得も簡単に行えてテスト実行レベルでは何の問題もない。
ただ実際にBOTなどを稼働させてみるとたまにstreamが切断されて即時再接続を行おうとしてエラーコード420(速度制限)が返ってくる挙動がみられる。例えばアプリがクラッシュしたときにservice化などで自動再起動設定にしていて即時再接続を行うと、エラーコード420を延々繰り返してゾンビ化してしまう（しまった）。
##解決策
twitterのドキュメントを調べてみると420が返ってきた場合（と他のエラーの場合）の再接続のベストプラクティスが示されているのでそれに従う。またstreamも一度にひとつだけ開くようにする。
Back off exponentially for HTTP 420 errors. Start with a 1 minute wait and double each attempt. Note that every HTTP 420 received increases the time you must wait until rate limiting will no longer will be in effect for your account.
参考: https://developer.twitter.com/en/docs/tweets/filter-realtime/guides/connecting
HTTP 420 errorの場合インターバルを置き再々接続までの時間を最大値まで指数関数的に増やすようにと指示されている。(例：60秒,120秒,240秒,…3600秒)
node-twitterでは実装の例などは無いので自分で実装する必要があるが、issue/159にコードの例があり参考にできる。　https://github.com/desmondmorris/node-twitter/issues/159
ただしここにおけるstream destroy()の問題は解決しているようだしsetTimeout()もPromiseでラップして書きたいので書き直してみた。
##実装
```js:recconect.js
'use strict';
/**
 *  streamを使用していて切断された場合即時再接続を行うとエラーコード420(連続して接続しすぎ)が返って接続できない場合がある
 * 即時再接続を行う設定の場合、エラーコード420が永遠に連続してしまう。
 * またstreamもひとつだけ開くようにする必要がある。
 * twitterのドキュメントでは420が返ってきた場合に再接続を行うときは
 * インターバルを置き再々接続までの時間を最大値まで指数関数的に増やすようにと指示されている。(例：1秒,2秒,4秒,8秒,16秒...3600秒)
 * node-twitterでは実装の例などは無いので自分で実装する必要がある。
 * issue/159にコードの例があり参考にできる。　https://github.com/desmondmorris/node-twitter/issues/159
 * ただここにおけるstream destroyの問題は解決しているようだしsetTimeout()もPromiseで書きたいので書き直す。
*/

const Twitter = require('twitter');

const client = new Twitter({
  consumer_key: process.env.TWITTER_CONSUMER_KEY,
  consumer_secret: process.env.TWITTER_CONSUMER_SECRET,
  access_token_key: process.env.TWITTER_ACCESS_TOKEN_KEY,
  access_token_secret: process.env.TWITTER_ACCESS_TOKEN_SECRET
});

var timeintervalsec = 1;
var exponent = 0;

const wait = ms => new Promise(resolve => setTimeout(resolve, ms));

// インターバルを2^0SEC,2^1SEC,2^2SEC...としてmain taskを実行する実装
function main() {
  timeintervalsec = 60 * Math.pow(2, exponent);
  wait(timeintervalsec * 1000)
    .then(() => {
      let date = new Date();
      console.log(date + ' Reconnecting... Interval Time = ' + timeintervalsec + " sec")

      var stream = client.stream('statuses/filter', { track: 'something' });
      stream.on('data', function (event) {
        exponent = 0; //初期化
        console.log('maintask');
      });

      stream.on('error', function (error) {
        if (exponent > 6) { // exponentが6以上つまり 2^12=3840 > 3600sec=1hourの場合処理を終了する
          console.log('再接続間隔が1時間以上となったため終了');
          throw error;
        } else if (error.message == 'Status Code: 420') { // 420の場合はインターバルを増やして再接続する
          stream.destroy(); //多重起動を防止するため
          exponent++;
          main();
        } else {
          console.log('420以外のエラーによる終了');
          throw error;
        }
      });

    })
    .catch();
};

main();

```
##議論
- コードは420の場合のみなので他のエラーの場合も実装してもいいかもしれないが、素直にクラッシュさせたほうがいいと思ってこのまま使用していたりする。
- twitterから420が返ってこないとテストできない。そこで邪道かもしれないがソースコードの420のところを401にしてKEYを一部変更して起動するとtwitterから401 errorが返ってくるので指数関数的に間隔をあけて、けなげに再接続を試みているのを観察できる。わざと420を起こしてBANされたくないしどうするべきか。

##参考
https://twitter.com/tipnem_faucet/with_replies
作成したBOT。twitterで指定のキーワードをつぶやくと仮想通貨を投げてくれる。
ソースコード
https://github.com/ocknamo/tipnem_faucet
似たような問題に対処してる方
https://57pfive.github.io/post/node_twitter/