CoffeeScript(1.7+)で書いたテストをmochaで実行する

mocha

メモ。
検索しても古いやりかたしか出てこなかったので。

mochaはnode.js製のテストフレームワークです。
CoffeeScriptはJavaScriptのトランスパイラです。

CoffeeScriptで書いたテストをmochaで実行できるのですが ドキュメント によると、 CoffeeScript 1.7からそのコマンドが変わっています。

coffee-script is no longer supported out of the box. CS and similar transpilers may be used by mapping the file extensions (for use with –watch) and the module name. For example –compilers coffee:coffee-script with CoffeeScript 1.6- or –compilers coffee:coffee-script/register with CoffeeScript 1.7+.

というわけで、以下のように実行します:

$ mocha --compilers coffee:coffee-script/register

古いやり方で出るエラー

$ mocha --compilers coffee:coffee-script
/.../node_modules/coffee-script/lib/coffee-script/coffee-script.js:195
          throw new Error("Use CoffeeScript.register() or require the coffee-s
                ^
Error: Use CoffeeScript.register() or require the coffee-script/register module to require .coffee.md files.
  at Object._base.(anonymous function) [as .coffee] (/.../node_modules/coffee-script/lib/coffee-script/coffee-script.js:195:17)
  at Module.load (module.js:355:32)
  at Function.Module._load (module.js:310:12)
  at Module.require (module.js:365:17)
  at require (module.js:384:17)
  at /.../node_modules/mocha/lib/mocha.js:184:27
  at Array.forEach (native)
  at Mocha.loadFiles (/.../node_modules/mocha/lib/mocha.js:181:14)
  at Mocha.run (/.../node_modules/mocha/lib/mocha.js:393:31)
  at Object.<anonymous> (/.../node_modules/mocha/bin/_mocha:380:16)
  at Module._compile (module.js:460:26)
  at Object.Module._extensions..js (module.js:478:10)
  at Module.load (module.js:355:32)
  at Function.Module._load (module.js:310:12)
  at Function.Module.runMain (module.js:501:10)
  at startup (node.js:124:16)
  at node.js:842:3

Node.jsにおける大量のデータ処理の際の非同期処理コールバックがスタックしすぎる問題への対処

ストリーミング処理の落とし穴

Mongoose(MongoDBのODM)を使って、コレクションからほんの10万ほどのドキュメントを処理するスクリプトを実行した時でした。処理が固まってしまうのです。なぜでしょう?

各ドキュメント処理では、コールバックを伴う非同期的な処理を含みます。
問題のCoffeeScriptのコードは以下のようなイメージです:

Foo.find()
    .sort value:-1
    .stream()
    .on 'data', (d)->
        someTask d

someTask = (d)->
    doAwesomeJob (result)->
        console.log "Great!"

モデルFooのドキュメントを全て読み込み、各ドキュメントをストリームに流し込んでいます。
メモリを節約するためにストリームの手法を採用しています。
doAwesomeJobは、例えばAPIを叩くとか、そういう非同期な処理です。
しかし、doAwesomeJobのコールバック関数がなかなか呼ばれない。遅い。ついには全く呼ばれなくなる。

イベントループのキューが溢れている

Node.jsアプリケーションはシングルスレッドです。
下図のように、イベントループ機構が備わっていて、Event Queueに溜まった処理キューを順次消化していく仕組みになっています。

event-loop

(via http://misclassblog.com/interactive-web-development/node-js/)

つまり、Node.jsはひとつずつしかキューを処理できないのです。
上記スクリプトで、ストリームのイベントのInvocationが、大量にこのイベントキューに押し寄せている事を想像してください。
doAwesomeJobのコールバックは、その非同期処理の完了時点で最後列にエンキューされます。
しかし、非同期処理の間にドキュメント読み出しイベントの処理キューが大量に溜まります。
その結果、doAwesomeJobのコールバックが呼ばれるまでに時間がかかってしまっているのでした。

似た問題に直面している人がいました。

RabbitMQのメッセージキューがいちどに大量に来た時に、イベントループのキューが溢れるようです。
問題は僕のケースより少し深刻なようです。見なかったことにしましょう

ストリームを分割しよう

溢れるのなら、分割すればいいですね。
以下のように変更を加えました:

processNext = (offset, num, on_completion)->
    i = 0, j = 0, closed = false
    Foo.find()
        .sort value:-1
        .skip offset
        .limit num
        .stream()
        .on 'data', (d)->
            ++j
            someTask d, ->
                ++i
                if i==j && closed
                    processNext offset+i, num, on_completion
        .on 'close', ->
            closed = true
            on_completion() if j==0

someTask = (d, callback)->
    doAwesomeJob (result)->
        console.log "Great!"
        callback()

processNext 0, 100, ->
    console.log "done!"

ドキュメントを100個ずつ読み出して、処理して、終わったら次の100個・・というフローに変更しました。
今のところ、これで上手く動いています。

でもこのコードは少し汚いので、よりエレガントに書くためにasyncを使うことをお薦めします。
drainで完了をフックするといいと思います。

このストリームの分割では、ストリームを処理している間にドキュメントが追加・削除される事を想定していません。
コレクションの変更まで考慮する場合は、想定される変更内容によって変わると思います。

ご参考まで!