トップ 追記

2020-09-08 [長年日記]

_ JavaScriptでバックグラウンドで処理してもらうのはPromiseをawaitしなければよいだけだった

当たり前じゃん、と思うじゃろ? それが他の言語のコンテキストを強く頭に持っているとすぐに発想できんのじゃよ、という話。

やりたいこと

長時間実行するのを前提にした Web アプリ、Web アプリというか単にバッチ処理を PaaS で動かす際に entry point が HTTP に露出しているだけのものがあって、その際、時間の掛かる処理の完了を待たずに 202 Accepted や処理中を意味する status をさっさと返してしまいたい。

前提

  • HTTP request については適切にアクセスが制御されており、不必要な request が大量に来ることはない
  • 長時間掛かるので内部で state を管理する必要があるが、その部分は完成しているものとする

例えばRubyだと

最もカジュアルな方法を、例えば Sinatra っぽい何かと日本語で書くとするとこんな感じになるかな。

post '/' do
  if (state は処理中ではない)
    Thread.new {
      storage に処理中の state を保存
      longrun_process()
      storage に完了の state を保存
    }
    [202, {}, []]
  else
    [412, {}, []]
  end
end

def longrun_process
  ...
end

Thread を使って一応期待通りの挙動になることは確認できた。

これを JavaScript でやりたい。

結論

express っぽい何かと日本語の擬似コードで書くとこんな感じになる。

post('/', async (req, res) => {
  if (state は処理中ではない) {
    await (storage に処理中の state を保存)
    longrunProcess().catch((e) => { throw e }) // <- await を付けない!
    await (storage に完了の state を保存)
    res.status(202).send()
  } else {
    res.status(412).send()
  }
})

ポイントは「待たない処理について await を付けない」これだけ。

ただし、ちゃんとケアしないと UnhandledPromiseRejectionWarning が出るので注意。

間違った考え方

async/await の世界に慣れて、非同期も待てるのが前提の頭になってしまって以下のように考えていたが、これは間違っていた。

  • Thread を使えばよいのか?
  • Fiber を使えばよいのか?
  • え、もう setTimeout({}, 0) でよくない?

JavaScript でのこれらの言葉は Ruby や他の同期的な処理を基本にした言語で考えていた使い方とはだいぶ異なることが分かった。

  • Thread
    • JavaScript の Thread は Worker を形成するための JavaScript のファイルを用意してそこから Worker を立ち上げることから始まる。当然名前空間は完全に丸ごとベツモノになり、呼び出し側で require 済みの module なども何も利用できない。
    • カジュアルさに欠けるし、やりたいことを実現するためのコード量はだいぶ多くなる。
  • Fiber
    • https://github.com/laverdet/node-fibers
    • 名前からしてこれっぽいし、挙動としても完全に期待通りだったが、ドキュメントにはできれば使うなと書かれていて、確かに不要だった。

ちなみに node-fibers を使ったコードはこんな感じになる。

Fiber(async () => {
  // ここにバックグラウンドで動いてほしいコードを書く
}).run()

「await を付けない」よりは明示的でとても嬉しいコードに見える。現在の Promise 前提のコードでも意図的にこういう wrapper を作ることは恐らく可能だと思う。

ちなみに、setTimeout({}, 0) を使う方法でも概ね似た挙動を実現できるんだけど、時間の掛かる処理に入る前にその下に書いたコードが動いてしまうのと、意味合いをつかみにくいのであんまりよくないかなぁということを現時点では考えている。なんとなくイヤなにおいがする。

まとめ

JavaScript はもともと非同期でした。async/await に慣れてほとんどすべての場合で await を付けて処理を書くようになってしまった今こそ、await を付けない挙動を今一度確認し、必要な場合は外しましょう。

おまけ

Worker Thread の上の特徴を理解したうえで完全に CPU bound な処理だけを background に回したい場合は以下の microjob という npm が使える。

https://wilk.github.io/microjob/

データだけは data や ctx で渡せるので、必要な module が何もない場合は以下のように簡単に使える。これはREADME のコードを端折ったもの。

(async () => {
  const { job, start, stop } = require("microjob");

  try {
    // start the worker pool
    await start();

    // this function will be executed in another thread
    const res = await job(() => {
      ..
    });

    console.log(res);
  } catch (err) {
    console.error(err);
  } finally {
    // shutdown worker pool
    await stop();
  }
})();

依存しているものが一切ないことはほとんどないと思うけど、単に await を付けないだけよりは分かりやすく安全なコードを書けそうに見える。

Tags: JavaScript

2020-06-04 [長年日記]

_ GitHub Package Registry使ってみたけど見送った

GitHub Packages: Your packages, at home with their code

分かったこと

少なくとも npm package の registry 追加、変更はあんまりカジュアルじゃない

  • rubygems なら source 書き足すだけでいいけど、package.json にそういう記述がない
  • .npmrc で registry の設定を変更することはできる。これを npm.pkg.github.com にしておくと、存在しない package を自動で npmjs.org から取得するように fallback できる。が、yarn には影響しない

GitHub 側の問題じゃなくて npm の仕様の課題かなー。rubygems は昔からカジュアルにミラーサーバ用意できたんだけど、npm とか yarn とかめんどいね。


2020-06-03 [長年日記]

_ Bundler v2の動作の変更にやっと追いついた

Bundler 2はGemfile.lockに書かれているBUNDLED_WITHを尊重する

Bundler: How to Upgrade to Bundler 2

Version Autoswitch

Now that you have Bundler 2 installed, you should know that Bundler will automatically switch between version 1 and version 2 based on your application’s Gemfile.lock. If your lockfile was created by Bundler 1, your commands will be run by Bundler 1. If your lockfile was created by Bundler 2, your commands will be run by Bundler 2.

これが分かっていないと以下のような現象にビックリする。

  • Ruby 2.6 は bundler gem も default gem になっていて、そのバージョンは1.7
  • 2.5.x までの感覚で何も考えずに gem install bundler で 2020-06 時点の最新の 2系 を入れる
  • BUNDLED_WITH 1.x な Gemfile.lock を持つプロジェクトで bundle コマンドを使うといきなり 1.7 が動く

これで何に驚くかと言うと、「せっかく bundler 2 を入れたんだから bundle update して BUNDLED_WITH を更新したいのにできない!」です。

言い方を変えると Bundler 2 以降の世界では .ruby-version と同じように Gemfile.lock が機能するようになるんだけど、direnv や rvm のようには「bundler の動作バージョン変わったで」とは教えてくれないし、Gemfile.lock 内の BUNDLED_WITH だけ書き換えるようなコマンド(例えば rbenv local みたいな)はないので各位覚えておくように、ということでした。

フツーは Gemfile.lock を手で編集しようと思わないもんなぁ…。(昔は CI でゴリっと書き換えるみたいなアクロバットをやってたけど cf. WindowsでRails x Herokuと仲良くする - あーありがち(2016-09-26)

どの程度今さらなのか確認

ちなみに Bundler 2 が出たのは1年以上前の2019年1月でした。あらま。

Bundler: Announcing Bundler 2.0

以前のバージョンの Bundler では Gemfile.lock に書かれている BUNDLED WITH と異なるバージョンを入れていると bundle コマンド実行後にこの部分だけ diff が出てしまうことがよくあったし、一時期は CI が Bundler 2 に対応してないとかイロイロあったけど、今は安定しているし、Bundler 2 には auto-switch があることを覚えて頑張っていきましょう、ということっぽい。

Tags: Ruby RubyGems
本日のツッコミ(全2件) [ツッコミを入れる]

_ えりむ [> Gemfile.lock 内の BUNDLED_WITH だけ書き換える それが記事中リンクにもある `$..]

_ wtnabe [あーサブコマンドではなくオプションとしてはあるんですね。ありがとうございます。]


2020-05-31 [長年日記]

_ GFMのコードブロック対応のmarkdown変換コマンドとしてmarkedを使ってみることにした

Marked.js Documentation

別にハイライトされる必要はなくて、単に GitHub 用に言語名を埋め込んでいるとか、そういう部分に対応していればヨシ!

blockquote class="twitter-tweet">

https://t.co/K2SFka28zN これkramdownよりマシっぽい。

— wtnabe, yet another yak shaver (@wtnabe) May 31, 2020

とりあえずkramdownよりはパッとコマンド叩いた時の結果は良好。

Tags: Markdown Node