Subscribed unsubscribe Subscribe Subscribe

A-FRONT

技術ネタ中心にその他雑多なこと

Node.jsでもassertしたい

まえがき

この記事はリクルートエンジニアアドベントカレンダー12日目の記事です。

Node.jsでのAssertion

Assertionとは?

多くのプログラミング言語assert というメカニズムが実装されています。 assert はプログラムを記述する人が、絶対にコードのこの場所ではこの値が入ってくる、という意志を表明するための仕組みです。 例えば、以下の add(a, b) のコードを見てみると、

var assert = require('assert');

function add(a, b) {
  assert(!isNaN(a));
  assert(!isNan(b));
  return a + b;
}

この add(a, b) では、2つの引数、 a b に数値以外の値が入ってきた場合、特に何か特別な処置をしない限り Assertion Error を発生させ、プログラムを終了させます。試しにやってみると

add('string', 2);
assert.js:85
  throw new assert.AssertionError({
  ^
AssertionError: false == true
    at add (/Users/akito/workspace/tmp/assert-test/index.js:5:3)
    at Object.<anonymous> (/Users/akito/workspace/tmp/assert-test/index.js:10:1)
    at Module._compile (module.js:571:32)
    at Object.Module._extensions..js (module.js:580:10)
    at Module.load (module.js:488:32)
    at tryModuleLoad (module.js:447:12)
    at Function.Module._load (module.js:439:3)
    at Module.runMain (module.js:605:10)
    at run (bootstrap_node.js:420:7)
    at startup (bootstrap_node.js:139:9) 

このように add に非数の値を与えると、想定通りエラーが発生することがわかります。 この仕組みにより、想定以外の値が入ってきたときに適切にプログラムを終了させることができ、主に開発フェイズでバグの早期発見に役立てることができます。 詳しくは 契約による設計 などで検索してみてください。

Javascript / Node.jsのAssertion

この assert というメカニズムは多くの言語に実装されていますが、実はJavascriptには等価の機能はありません。console.assert() という関数はありますが、これは第1引数の値が false だった場合にconsoleにエラーメッセージを出力するもので、assert 本来の目的を達成するには少し弱いかなと感じています。

一方、Node.jsではcoreのモジュールとして assert が提供されています(上のサンプルコードではそれを利用しています)。この assert は上で見たとおり、表明した値以外の値が入ってきた場合にエラーを発生させ、プログラムを終了させることができます。

じゃあこの assert をプロダクションにガンガン記述し、他言語同様に堅牢なコーディングができるか、というと実はまだ少し足りない点があります。 assert が実装されている多くの言語では、プロダクションビルドもしくは実行時には assert はOFFにするのが一般的です。例えば、以下のJavaの例を見てみると、

public class Main {
    public static void main(String args[]) {
        String name = null;
        assert name != null; // ここでAssertionErrorが起きてほしい
        System.out.println(name);
    }
}
$ javac Main.java
$ java Main

> null

name の中身が null なので、本来は AssertionError が発生してほしいのですが、実際に実行してみるとなにも起きません。実はJavaは実行時にはデフォルトでassertはOFFになるように設計されています。Javaの場合、assertをONにするためには、実行時に -ea オプションを設定します。

$ java -ea Main

Exception in thread "main" java.lang.AssertionError
    at Main.main(Main.java:5)

今度は想定通り AssertionError が発生しました。

いくら堅牢なコードを書くための仕組みといっても、本番で動いているシステムがいきなり AssertionError で死んだら困りますよね。このように、Javaを始め多くの言語ではbuild、もしくは実行時にassertのON / OFFを切り替える機能がついていて、開発時、本番時で挙動を切り替えられるようになっています。

On The FlyでUnassertする

では一方Node.jsではどうかというと、悲しいことにこの機能が実装されていません。ですので、 assert に引っかかったら本番だろうがなんだろうが死んでしまいます。 ようやくこの記事の本題になりますが、今回は assert をNode.jsのプロダクションでも活用するために、動的に assert の ON / OFFを切り替える仕組みを実装していきたいと思います。

unassert

0から作るのはなかなか大変な作業ですが、幸い、id:t-wada さんが作成した、jsのコードベースから assert を除去するnpmモジュール unassert がありますので、こちらをNode.js向けに活用してなんとかしようと思います。

unassertBabelwebpack などのpluginとして動作します。最近のフロントエンド界隈ですとプレーンなjsを記述することは少なくなってきており、babelwebpackを用いてES2016 - 2017もしくはjsxなどをブラウザで動作するjsへとトランスパイルすることが多くなっています。unassert はこのトランスパイル時にコードから assert を取り除くのが主な使い方となってきます。

一方で、サーバサイドJavascriptのNode.jsでは、babelなどを使うケースは、フロントエンドほどは多くはないのではと思っています。1つに、Node.js(とv8)は言語系のキャッチアップの速度が非常に早く、標準になったシンタックスや機能はいち早く取り込んでいます。このサイトを見ても分かる通り、仕様が固まったものに関してはほぼほぼ実装されており、特別なケースが無い限り、 babel を使うメリットはそこまでないのかなと思います。そのため、 Node.jsには unassert がassertを取り除いているトランスパイルのフェイズが存在しないことになります。

requireをhookして実装する

じゃあどうするかというと、Node.jsがモジュールをロードする仕組みであるrequire をhookして、そのタイミングで unassert してしまおうと思います。 Node.jsでは実行時に--requireオプションを指定することができ、そこでモジュールを unassert した上でロードするようにrequireの挙動を書き換えてあげればうまくいきそうです。 コードはこんな感じになります。

var extensions = require.extensions;
var fs = require('fs');
var unassert = require('unassert');
var esprima = require('esprima');
var escodegen = require('escodegen');

function unassertLoader() {
  extensions['.js']= function(localModule, filePath) {
    var originalSource = fs.readFileSync(filePath, 'utf-8')
    var ast = esprima.parse(originalSource);
    var modifiedAst = unassert(ast);
    localModule._compile(escodegen.generate(modifiedAst), filePath);
  };
}

unassertLoader()

実際の複雑なところはすべて unassert がやってくれているのですごくシンプルですね。 流れとしては、requireモジュールの中のjsの拡張子がついてきているファイル用のloaderを書き換え、一旦esprimaでASTになおしてから、unassertし、再度escodegenでASTからコードに戻した上で、module loaderに渡してあげているという形です。

このコードを unassertLoader.js という名前で保存しておきます。

挙動チェックのためにこんなコードを用意しました。

// test.js
var assert = require('assert');
assert(false); // ここで必ず落ちる。
console.log('Hello');

動かしてみます。

$ node test.js
assert.js:85
  throw new assert.AssertionError({
  ^
AssertionError: false == true
    at Object.<anonymous> (/Users/akito/workspace/unassert-loader/benchmark/test.js:3:1)
....

当然ですが落ちますね。 では --require オプションを付けて先程書いた unassertLoader.jsをpreloadしてみたいと思います。

$ node --require ./unassertLoader.js test.js
Hello

今度はassertが削除されて、無事にconsole.log('Hello') が動作していることがわかります。 これで目的どおり、実行時の指定でNode.jsでも動的に、つまり、On The FlyでassertをON/OFFできるようになりました!

簡易ベンチマーク

ちょっと心配なのが、requireの度にASTにしてunassertして更にコードに直して、という処理を入れるのはパフォーマンス的に大丈夫なの?という点です。 実はNode.jsのrequireは一度読み込むとあとはすべてキャッシュから読み込むため、毎度AST変換などの処理は入らないので大きな影響はないというように予想できますが、しかし、推測するな計測せよの精神でとりあえず簡易的にですがベンチマークを取ってみたいと思います。 Benchamrk.jsを使ってこんなテストを書きました。

// bench.js
var Benchmark = require('benchmark');
var suite = new Benchmark.Suite;
var decache = require('decache'); // requireのキャッシュをclearするためのモジュール

let time = 0;
suite.add('Require testing', function() { // 1つめのテストケース
  require('./math')
})
.add('Require uncache testing', function() { // 2つめのテストケース
  decache('./math')
  require('./math')
})
.on('cycle', function(event) {
    console.log(String(event.target));
})
.run({ 'async': true });
// math.js
function add (a, b) {
  assert(!isNaN(a));
  assert(!isNaN(b));
  return a + b;
}

module.exports = add;

math.js という冒頭に出てきたモジュールをrequireさせ、ops (operation per sec)を計測します。 1つめのテストケースではrequireのベーシックなパフォーマンスを計測します。Benchmark.jsでは1つのTest Suitを複数回繰り返し、opsを計測するので、想定通りならば2回目以降のrequireはすべてキャッシュから返されるため、通常のケースもunassertLoaderをpreloadした場合もopsに影響を及ばさないと考えられます。 2つめのテストケースではキャッシュから呼び出されるのではなく、1回のrequireの処理のopsを計測します。1度requireしたモジュールを都度decacheして無効化しているので、このケースではunassertLoaderの1回目の処理が通常のケースに比べてどの程度遅延するのかを計測します。想定通りであるならば、通常のケースとunassertLoaderを用いたケースではopsに大きな差があると予測できます。

それでは実際に実行してみます。

$ node ./unaasetLoader.js bench.js
$ node --require ./unassertLoader.js bench.js

結果は以下のようになりました。

Case1 Case2
通常の起動方式 110,789 ops/sec ±1.37% 7,919 ops/sec ±2.34%
unassertLoaderをpreload 108,085 ops/sec ±1.33% 2,421 ops/sec ±2.13%

Case1では想定通り、通常の起動方式もunassertLoaderをpreloadした場合もopsに大きな差は見られませんでした。このことから、requireのキャッシュが十分に働き、パフォーマンスに大きな影響はないことがわかります。 Case2においても、想定通り、opsに3倍以上の差があることがわかります。実際のところ、decacheにかかるオーバーヘッドが計測できていないので正しい数値は出せませんが、やはりAST変換 ~ unassert ~ コード生成の一連の流れは大きな負荷になっていることがわかりました。

ベンチマークの結果から、通常用途であればunassertLoaderは問題なく使用できるが、もし何らかのケースでキャッシュを使えない場合は注意が必要、ということがわかりました(そんなケースあるのか?)。

あとがき

この記事ではNode.jsでも他の言語と同じようにassertを活用した堅牢なプログラミングを行うためにはどのようにすればよいか、ということを考察してきました。 この話を考え始めたきっかけは今年のNode学園祭でt-wadaさんのLTを聞いたことがきっかけでした。たしかにunassert使えばフロントではassert使えるけどサーバだとどうなるんだろう?みたいな感じです。

今回あまり触れることのないNode.jsのrequire関連の挙動について知見を深めることができました。これからは安心して、プロダクションにガンガンassertをいれられそうです。

今回作成したunassertLoaderは近々OSSとして公開する予定です、 公開したらまた報告したいと思います!

東京Node学園祭2016に参加しました

2016年11月12, 13日と開催された東京Node学園祭に参加してきたので、その報告をします。

nodefest.jp

 

 

登壇したはなし

僕は初日にLTしました。発表資料はこれ。

speakerdeck.com

 

色々書いてありますが、

  1. Node.jsを始めとするJavascript界隈は特に言語の変化が激しい。
  2. 今書いているES2015やES2016だっていつかLegacyになるときがくる。そのときに、どうやって安全に移行しますか。
  3. 色々方法はあるけど、TDDで継続的リファクタの環境や土壌、文化を今から作っておくのは1個の正解じゃない?

っていうことが言いたかったことです。そう考えるとタイトルミスった感が。まあしょうがないね。

 

この話をしようと思った背景

2つあります。

1つめは、ここ半年くらい月1でとある著名な方とTDDのペアプロをさせていただく機会があり、色々と学んだこと、感じたことを一度言語化してまとめたかったからです。

2つめは、まさに今業務であまりメンテされてこなかったNode.jsのシステムをちくちくリファクタしつつ新しいNodeのバージョンに移行する作業をやっていて、そのときTDDの考え方に大分助けられている実感があり、それを共有したかったからです。

 

1つめに関しては、やりたいことは概ねやれたと思います。やっぱり言語化すると色々と知識が整理されて良いですね。正しく発表するために色々と調べるし。

2つめに関しては正直この問題に直面している人がどれくらいいるのかわからないので、なんとも感触がありません。。。

今回、業務で古いシステムをリファクタする際、真っ先に手をつけたことはテストが不安定もしくは壊れているコードの修正でした。とにかく正しいテストが正しく回る状況を作り出し、あとは単体テストに守られながらまずい切り方をしているAPIを直したり、サポートされなくなったnpmモジュールの置換などを行ったりしました。その方針はかなりうまくいっているかなと思っていて、現在のところスケジュールもまきで行けているし、大きな手戻りも発生していません。ココらへんの話はプロジェクトが終わったらまた別でできると良いですね。。

 

印象に残ったセッションのはなし

ひとつめ: Douglas CrockfordのKeynote

Seif Projectの話でした。

The Seif project - O'Reilly Media

構想自体は壮大すぎてもはやピンと来なかったレベルなんですが、

 

「WWWはドキュメントをdeliverするための仕組みであり、Applicationをdeliverする仕組みとしては必ずしも最適ではない」

という話はすごい共感できました。

特に、2日目午後にやったVue.jsとReact-Reduxの話を聞いてもった感想がこちら

 

ふたつめ: Boarding the tiny framework train by @yoshuawuyts

http://tacit-fly.surge.sh/

Yoshuaさんの話です。chooの開発裏話から、Yoshuaさんのvim力が卍解してたライブデモまであってとにかく楽しめた発表でした。

choo、チュートリアルだけやったことあるのですが、react-reduxをベースとしていながらもシンプルな作りで変なところで悩まないすごく好感が持てる作りでした。フロントエンドのFWが色々出てきている中で、流行るかどうかはわかんないですけど、積極的に使っていきたいなあと思います。なにせ書いてて楽しいし。

 

chooのリポジトリはこちら

github.com

 

みっつめ: How Do We Get Along With Static Types by @__gfx__

How Do We Get Along With Static Types // Speaker Deck

jsと静的型の話です。

jsと型の話は少し考えるところがあって、懇親会でも「jsは型は必要だと思うか?」みたいな質問をしたんですが。。。

懇親会で色々な人に話を聞いた結果、型になにを求めるかは人によって様々で、IDEでのリファクタやAutocompleteをより効率的にやるために型が欲しい人から、型によってよりプログラミングの表現力を向上させたい人まで、色々な幅がありました。まだjsに型をつける系のやつ全然触っていないので、Flowtypeあたりから試して見ようかなあ。。。

 

これ以外にもたくさんのセッションがあり、それぞれ実践的かつ濃い内容ばかりで、色々刺激を受けました!特に、Debugging Node.js Performance Issues in Productionで話していた内容はすぐに業務に活かせそう。

 

さいごに

国内外スピーカーとの調整や、当日の同時通訳など、限られた時間と予算の中で実行するのは大変に困難だったかと思います。運営の皆様の配慮とホスピタリティのお陰で、非常に楽しめた2日間になりました。

この場を借りてNode学園祭の運営の皆様にお礼を言いたいと思います、
本当にありがとうございました!

 

まとめ

Node学園祭でTDDのはなししてきた。リファクタがキマると気持ちいいです。

 

 

あ、あとCode and Learnでおくったパッチマージされるといいなあ。

 

はじめました。

技術ネタを中心に、勉強したことをアウトプットしていきたいと思います。