🍆

Async.jsで再帰処理を実装するときに注意したいこと

2024/09/16に公開

Async.jsを使って並列実行をしている関数で、再帰処理を書いたところキューの管理がうまくいかなかったことがありました。その解決策が分かりましたのでメモとして残しておきます。

Async.jsとは

https://www.npmjs.com/package/async

最大同時実行数を指定したり、タスクを動的に渡したりできる並列実行の定番ライブラリです。スクレイピングの際にも重宝します。

どんな問題が起きたのか?

12台ほどのAndroid端末を接続し、Playwrightでスクレイピングしていました。
例外エラーが発生したら再帰処理を行っているのですが、再帰処理が動いたにも関わらず asyncQueue.length()asyncQueue.running() の値が減っていることに気が付きました。

自作関数自体は再帰処理を継続しているが、Async.jsに正しく再起処理中であることが伝わっておらず、キューの管理が不完全で、タスクが完了したと思われてしまっている状態です。

Async.jsが適切に動いてくれない原因になるので修正しなければなりません。

原因

class Scraping {
  constructor () {}

  async startScraping () {
    try {
      await this.start();
      await this.quit(); // スクレイピングを完了する
    } catch (e) {
      // エラーが起きたのでリトライ処理を行う
      await this.restartScraping();
    }

    // 関数的には restartScraping() を実行した後、
    // ここまで到達してしまい、startScraping() 関数は終了したという扱いになってしまう
  }

  async restartScraping () {
    await this.closeBrowser(); // ブラウザを閉じる
    await this.startScraping(); // 再度スクレイピングを実行する
  }
}

このような書き方をしていたのが原因でした。
例外エラーを検出すると、 restartScraping() で再起処理を行うのですが、関数的には .restartScraping() を実行したら startScraping() は完了したことになります。

完了していないことにするには、 startScraping() を完遂させないような書き方をしなければなりません。

解決例

付け焼き刃な修正ではありますが、下記のようにすれば解決できました。

class Scraping {
  isComplete = false;

  constructor () {}

  async startScraping () {
    while (!this.isComplete) {
      try {
        await this.start();
        await this.quit();
        this.isComplete = true;
      } catch (e) {
        // エラーが起きたのでリトライ処理を行う
        // リトライの準備を行う
        await this.closeBrowser();
      }
    }
  }
}

startScraping() を while文でロックする形にしました。
try内の一番最後まで到達したら処理が終わったとみなし isComplete をtrueにします。
trueになればwhile文を抜け出すことができるので、ここで startScraping() が終了したことがAsync.jsに伝わります。

例外エラーが起きて再起処理になるときは、isComplete は false のままですから、そのまま catch の中の処理が行われて、while文の冒頭まで戻ります。そのため、再帰処理が発生すると startScraping() からは抜け出さないので、Async.jsにはタスク実行中のままであると思わせることができます。

おしまい

Async.jsは書き方を間違えると予期せぬ動きをしてしまいそうです。
再帰処理を含む記述をする際は 「関数の最後の行まで進んでしまわないか」 に気を配りながら実装するとよいでしょう!

Discussion