トップ 最新 追記

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-09-27 [長年日記]

_ NestJSでInjectされるオブジェクトの初期化のタイミング

実はここのところ NestJS を触っていました。これについての感想などはまたいずれ書くとして、DI コンテナそのものに不慣れだったので NestJS の DI でとても悩みましたという話を少々。(例によって分かってしまえばどうということはないんだけど)

※ 一応間違いないはずだけど、思い違いがあれば教えてください。

以下に(module定義を除いて)DI コンテナに登録する provider とそれを利用するコードを挙げる。仮に以下のようなコードだったとして、実際にどの部分のコードがどの順番で実行されるのかを確認し、それによって生じる制限と、その制限を克服する方法を整理する。

コード例

https://nestjs.com/

NestJS は TypeScript で動いてます。

@Injectable()
export class Dependee {
  constructor () {
    (1)
  }
}
import { Dependee } from '..'

export class Depender {
  constructor (
    dependee: Dependee
  ) {
    (2)
  }
}
  • Depender が Dependee に依存している
  • Depender の constructor で Dependee が inject される

実行順

  • Dependee の constructor が実行され(default の provider の挙動)て、インスタンスが生成される
  • 生成されたインスタンスが Depender の constructor で inject される

できないこと

常に Dependee が先にインスタンス化されるので、Depender の constructor で取得できる値を Dependee の constructor に渡す方法がない。

具体的にどう困るのか

例えば Controller で @Query や @Param で取得した値から「特定の値を持つ provider のインスタンスを生成する」ことは組み込みの DI の機能では実現できない。これは Controller の constructor が実行されるタイミングですでに Dependee のインスタンスは生成されていて、Action に該当するメソッドが呼ばれるタイミングは二周遅れになっているため。

ではどうするとよいのか。

Dependeeに値を渡す方法

どうにかして値を渡したい場合、どうするのがよいのか。方法としては大きく分けて2つ、全部で4つくらいありそう。

  1. 最初にあり得る値をセットするインスタンス生成法を全部 DI コンテナに登録しておいて必要なものを必要な人が取得する
  2. Inject の機構を利用せずに Depender の constructor の中で手動で Dependee をインスタンス化する
    • DI になっていない*1
  3. 実行する処理そのものが書かれているメソッドに渡す
    • property ではなくメソッドのシグニチャで class の特徴を表すことになる
    • 複数のオブジェクトにそれらを渡しながら処理していく場合にインターフェイスの変更が高コストになるのでカッチリ設計が固まっている場合以外は工夫が必要。例えば context オブジェクトを導入するにしても今回の constructor の実行順の問題は同様に残る。
  4. constructor に渡すのは諦めてsetter を用意して setter で値を渡す
    • この場合、Dependee の該当 property は readonly にはできなくなる
    • 言語の機能で immutable にはできないので、どうしてもこだわるなら何らかの工夫が必要
どうしてもインスタンスの初期値を最初に消めたのちは途中で挙動が変わってほしくない場合

1 は DI コンテナの機能も素直に活かしつつ、初期値で挙動を固定して途中で変わってほしくないという要望も完璧に満たすことができる。具体的には NestJS では provider 定義に useFactory を使うとこうしたことが可能となる。

Custom providers | NestJS - A progressive Node.js framework

ただし、例えば日付やお金など値の範囲が無限になってしまうものに対してはこの方法は使えない。曜日くらいなら可能。

2 は Dependee の挙動をインスタンス生成時に固定するという目的に対して最も解決が早く、新たな学習コストがない。ただし、用意された DI の機能は使っていないのでちゃんと依存関係の管理ができているかというとあやしくなってくる。

インスタンスの挙動が変わることを許容できるなら

いちばん素朴で素直なのは 3 かな。インスタンスそのものが何をするのか知っているという形ではなくなってしまうけど、その部分の責務をすべてメソッドに担わせる形。

副作用として interface でちゃんと設計を練ってあげてそれを type として利用するようにしておくと class そのものが変わっても耐えられる。こうなると本当に DI っぽい。

4 は 3 と似てるけど setter があるとメソッドの呼び出し順に依存するので避けられるなら避けた方がよさそう。setter で丸ごと放り込むのが雑にやるには早いのは早いけど。

*1 とは言え DI コンテナを使えば DI なのかと言われるとやや疑問は残る。依存先のオブジェクトの生成方法は隠蔽されているが、interface ではなく provider の実装そのものを type として指定している場合に、どこまで DI と呼んでよいのか…? いずれにせよ絶対にコンテナに乗っていないといけない、あるいは乗っていればよいと考えるのも何か違う気がする。ただし、NestJS の場合は DI コンテナに依存するために module 定義に乗せておくと compodoc https://compodoc.app/ というツールで依存関係を visualize できるので、これは大きい。また、どこで何を利用しているのかが実行前に分かればリファクタリングは行いやすい。