node-fiberでライフチェンジングとか煽ったことを若干後悔してる
承前→ node.jsのvmとsynchronizeはライフチェンジング - taiyoh's memorandum
@hokaccha先生の記事読んで、やっぱインフルエンサーが書くと内容の充実度も反応も全然違うなー、とか思ってたのですが、特にあの記事をぶくましてるけどfiber使ったことない人は、使いどころをちゃんと見極めた方がいい、という注意喚起。もしくは過去の自分の記事に対する懺悔。
実は以前qiitaにこっそり投稿してたんだけど、以下の処理の時にエラーが発生する。
// sync_test.js var sync = require('synchronize'); sync.fiber(function() { try { sync.await((function(defer) { console.log("await 1 start"); setTimeout(function() { console.log("await 1 end"); try { sync.await((function(defer2) { console.log("await 2 start"); setTimeout(function() { console.log("await 2 end"); defer2(null, 'ok'); }, 1000); })(sync.defer())); console.log("await 2 over"); defer(null, "ok"); } catch(e) { console.log("await 2 error", e); defer(e, null); } }, 1000); })(sync.defer())); } catch(e) { console.log("await 1 error", e); } console.log("await 1 over"); });
ずらずら長いが、端的に言えば、sync.await中にもう一回sync.awaitしたらどうなるか、ということを検証したもの。結果は以下のとおりになる。
% node sync_test.js await 1 start await 1 end await 2 error [Error: no current Fiber, defer can'b be used without Fiber!] await 1 error [Error: no current Fiber, defer can'b be used without Fiber!] await 1 over
これだけだとピンとこないかもしれない。じゃあ以下のパターンがあった時は?
- ちょっと大きめのDBのselect文をsync.awaitで発行してる最中に、別のリクエストでsync.awaitを使ってファイルへの書き込みをする
- OAuth認証の処理をsync.awaitで発行してる最中に別のリクエストでsync.awaitを使ったselect文を発行する
他にも色々あると思うけど、ふつーにこういうウェブアプリケーション書いてしまうと、確実にエラーが発生し、「テストでは通ってるけど、本番に上げたらなんかちょくちょくエラーが起きる」という事態が発生する。シングルスレッドでこなすnodeならではの現象だと思う。PerlとかRubyみたいにpreforkしてリクエストを処理したらどうなるかはわからない(そもそもできるのか?このへんは自分はよく知らない)。でも、そういうアプリケーションを書く人は、よっぽどの変態か「自分はJS以外の言語を知らないから」という消極的な理由でアプリケーション実装を選択してるか以外にないだろうと思う。
要は、プロセス中のシーケンスが単一であればいい。なので、バッチスクリプトを書くときなんかは、すごく効果的なんじゃないかと思う。バッチ実装で非同期とか考えないでいい筈なのにコールバック地獄とか、ただの面倒事でしかないので。
ただのメモ書き、またはArk::ModelsライクなものをAmon2でも用意する
調子に乗ってAmon2::Modelsとかあるといいかも、って思ってたけど、
package MyApp::Models; use strict; use warnings; use Object::Container '-base'; sub import { $_[1] = 'model'; # or something goto \&Object::Container::import; } register config => sub { shift->ensure_class_loaded('MyApp::Config'); MyApp::Config->current; }; package MyApp::Config; use Config::ENV 'PLACK_ENV'; 1;
これで大体やりたいことはできる感じがしてきた。
Emacsのflymakeでcarton+plenvな環境でも無理矢理@INCを通す
なんか最近Perlづいてます。今日は環境周り(Emacs)。
久々にちゃんとflymake使おうと思ったので、karupaneruraさんの作成したplenv.elを使って、plenvで構築したperlバイナリでチェックかけるようにしました。ただこれだけだと、cartonでインストールしたモジュールに対してパスが通ってなくて、エラー扱いになるっぽいんですよね。。。
なので、以下のようなelispを追加しました。再帰的にパスを辿ってファイルを見つける、という処理は
→ Rakefileを再帰的に探してrake testするelisp(TB) - trotrの日記
こちらのエントリにあるRakefileを見つける関数を使わせてもらいました(ありがとうございます><)。
elisp全然慣れてないとかもあって相当力技ですが、cartonとcpanfileの有無をチェックして、cpanfileとcartonがあるときはcarton execを使って、それ以外は通常どおりguess-plenv-perl-pathで出てきたバイナリを使う、という流れです。絶対どっかで無駄なことしてるだろうな。。。探索し過ぎだし。PERL_CARTON_PATHも使ってるから、carton execする意味が本当にあるのか謎だし。ということで、elisp書ける人はもっとキレイに書いていただきますよう、宜しくお願いします。あと、関数の名前空間汚したくないので、myenv-〜とか適当に入れて書くようにしてますが、この辺って今は何がナウい書き方なんだろ。。。
tokuhirom氏のリファクタリング見てすげーなー、と思った、って話
typo氏からも「はよ」とか急かされたので、裏ですったもんだした挙句CPAN Authorになってしまいました。
→ http://search.cpan.org/~taiyoh/
どうぞ宜しくお願いします><
さて、本題ですが。
今回Amon2::Web::Dispatcher::RouterSimple::Extendedというモジュールを上げたのですが、エントリ書いた時にgfx氏から「no strict 'refs'の状態でコードをガリガリ書くのはマズい」と指摘を受けました。僕も書いてる時からそれは薄々感じてはいたんですが、有効な策が打ち出せず、お茶を濁すような対応しか出来てなかったわけです。なので、追記の形で泣きついてみたら、翌朝tokuhirom氏からコメントをもらい、「こんなやり方がある」と紹介してもらいました。
→ https://github.com/tokuhirom/p5-Amon2-Web-Dispatcher-RouterSimple-Extended/blob/refactoring/lib/Amon2/Web/Dispatcher/RouterSimple/Extended.pm
Amon2::Web::Dispatcher::RouterSimple::Extendedでは0.02で上記の氏のリファクタしたものを手動マージで採用し、0.03では更に自分なりに修正したり、バグフィックスを加えています。
氏のリファクタリングしたソースを見て気づいたことで、一番考えなくちゃいけないのは、「なぜ"no strict 'refs'"をしなきゃいけないか」ってところだなと思いました。見ての通り今回のソースでは、呼び出し元に対して関数を追加しているわけですが、初期の僕の実装でやっていたのは、取り急ぎの追加であって、どのタイミングで関数を追加/変更するかについて、全然考えられてなかったのですね。場当たり的につけたり取り替えたりしてる。しかし氏のソースだと、追加する部分は一箇所にまとめられ、他の場所で関数を付け替えたりしていることは殆どない。submapのインスタンスの有無で、関数内の挙動を変化させている。あと、gotoをうまく使ってて、これはシラフでも全然思いつかなかったです。今後DSLっぽいのを書く時使えそうなので、意識してみたいと思いました。
あと、0.03ではリファクタリングしてもらったソースに対して2点変更したところがあって、一つは単なるバグ修正です。もう一つはsubmapのインスタンスの持ち方についてで、local使えなくてかっこ悪くなってもいいから、モジュール内の変数でキャッシュさせる方式にして、submapの定義が終わったら明示的に消すようにすればいいや、という風にしました。そうすれば、no strict 'refs'はimportの中だけになるので。
あとはpod周りだなー。今まで書き捨てみたいな感じでしかやってなかったので、突然cpanに上げることになって相当てんやわんやしておりました。ってか今もしてる。正直読みたくないけど、気づいたらボチボチ直していきます。。。
それにしても、週末わざわざコメントしてもらったりリファクタリングまでしてもらい、ありがとうございました > tokuhirom氏
Amon2のDispatcher::RouterSimple、個人的にこんなのだといいなー、という例
[追記]
tokuhirom氏に「よさげ」って言ってもらえたので、調子にのってgithubに上げました(シーパン王サーじゃないので)。Amon2::Web::Dispatcher::RouterSimple::Extendedって名前にしております。::RailsLikeとかも考えたけど、関数名違うから混乱するのでやめ。そして自分の英語がひどすぎて死ぬ。あとモジュール名長すぎる。
https://github.com/taiyoh/p5-Amon2-Web-Dispatcher-RouterSimple-Extended
[追記終わり]
[追記2]
gfx氏から「no strict 'refs';状態でガリガリ書くのはマズイ」という指摘を受けて、確かにそうだよなー、でも、submapperへの出入りでコロコロ切り替わるから、限定するのはムズいよなー、と思ったので、関数はある程度他所で定義するようにして、あとは力技でuse strict 'refs'をこまめに入れて厳密性を高めるようにしてみた。ちょっと僕の技量だとこれが限界なので、もっといい実装あればアドバイス or pull-reqください。。。><
https://github.com/taiyoh/p5-Amon2-Web-Dispatcher-RouterSimple-Extended/commit/8fd8e02cfb31bfb852644f163aeae24f251a85c9
[追記2終わり]
久々にPerl書いております。
Amon2でやってみようかと思って色々読んでいるのですが、Router周りはRails的な方が個人的には好みだなー、と思ったので、こんな風に変えてみた。一応テストも書いて通している。
こうすると、
package MyApp::Web::Dispatcher; use strict; use warnings; use utf8; use Amon2::Web::Dispatcher::RouterSimple; connect '/' => 'Root#index'; # API submapper '/api/' => API => sub { get 'foo' => 'foo'; post 'bar' => 'bar'; }; # user submapper '/user/' => User => sub { get '', 'index'; connect '{uid}', 'show'; post '{uid}/hoge', 'hoge'; connect 'new', 'create'; }; 1;
という感じで、色々関数を使えるようになる。最初はsubmapperの扱いがぞんざいだと思ったので、再定義して第三引数に定義した関数内もconnectを使えるようにしただけだったんだけど、欲が出たのでget/post/put/deleteも使えるようにしてしまった。
あとホントはantipop氏が1年前くらいのブログ記事で書いている通り、
ディスパッチされるコントローラのクラスが、::C::*というような名前で、僕はクラス名などを省略するのが個人的に嫌い
ぼくがかんがえたさいきょうのAmon2のつかいかた - delirious thoughts
というのに僕も賛成なのですが、まあ、もういいや、と。自分でRouterを再定義してそっちを使うようにすればいいので。
ともあれ、Amon2はわかりやすく、かつ、カスタマイザブルなWAFなので、とても使い易いものだと思います。これでちょっといろいろ試してみたいところです。
ぼくがかんがえたさいきょうのAmon2のつかいかた - delirious thoughts
ということなので、Amon2のコア部分とうまく付き合いながら、オレオレになりすぎないようにやっていこうと思っております。
結果だけじゃなくてプロセスも大切なんだよ、ってお話
「手段がどうかじゃなくて最終的に何をゴールにするかだよね」っていう言説をよく見る気がしていて、まあそれは真理だよねと思いつつ、どこかでずっと引っ掛かってたけど、やっとそこが言葉に出来た気がする。
一番気になっていたのは、上記の言葉がどういう文脈で語られているかという点だった。一般的には目的を論じる場なのに手段に偏った意見が出てきた時にカウンターとして使うけど、たまに、自称俺は目的やゴールが大切ですから細かいことは気にしません君が相手の意見を封じる際に使ったりもしてて、これが厄介なんだな、と。
端的に結論を言うと、僕があるべきと思う文脈は、
「目的やゴールが大切なんだから、手段とかどうでもいいんだよ」じゃなくて、
「手段は大切なんだけど、それに拘ってるとゴールを見失うから、常に気をつけていましょうね」って方だな。
本当にゴールに拘る人はそもそももっと大きなスケールで話をするし、
プロセスと結果の二元論で議論になるレベルの問題だったら、そもそもどっちがプライマリかとか、いちいち話す時間が勿体ない。
余談だけど、もっと抽象的にその先を話すと、プロセスを通してゴールに導く人も、ゴールを見定めて導く人もどちらも大切で、お互いに自分の役割に誇りを持ちつつ、もう片方をリスペクト出来るのがいいなあ、と僕は思う。プロセスを考えないといけない人が、ゴールにばかり目を奪われて自分の役割を疎かにしているとか、ゴールを見定めて導く人が、そのためのプロセスについて無関心だとか、そういうディスコミニュケーションは不幸の始まりなんじゃないかな、と。
更に煙に巻くことを書いておくと、ここは戦略と戦術の関係について話をしてるんじゃなくて、因果の話をしていますよ、と。
node.jsのvmとsynchronizeはライフチェンジング
[追記 5/29]
synchronizeというかfiberには以下の問題点があるのでそちらを参照の上、このエントリをご覧ください
→ node-fiberでライフチェンジングとか煽ったことを若干後悔してる - taiyoh's memorandum
[追記 終わり]
node.jsをお使いのみなみなさま、コールバック地獄の中をいかがお過ごしでしょうか。
最近になって僕はvmモジュールとsynchronizeモジュールを使い出しまして、これがちょっと尋常じゃないくらい自分の実装方法に影響を与えております。
百聞は一見にしかずということで、実際どんな感じで使っているか、最近作ったユーティリティファイルの一部を載せます。
// util.js var vm = require('vm') , fs = require("fs") , path = require("path") , sync = require("synchronize") , root = path.resolve(__dirname + "/../"); // rootは、実行するアプリケーションのルートディレクトリが指定できればいい // fs.readFileメソッドを"同期"処理にする!!! sync(fs, "readFile"); module.exports = { runScript : (function() { var cache = {}; return function(p, context) { if (!cache[p]) { cache[p] = fs.readFile(root + "/" + p, "utf-8"); } vm.runInContext(cache[p], this.createContext(context), p); }; })(), createContext : function(context) { context = context || {}; context.require = require; context.module = module; context.console = console; context.global = global; context.myutil = this; context.sync = sync; return vm.createContext(context); } }
これを実行ファイルか何かで
var myutil = require("./lib/util");
という感じで受け取っておき、アプリケーションに必要なコードは全てmyutil.runScript(ファイル名, コンテキストになるオブジェクト)でロードするルールにしてます。
vm.runInContextの一番の使いドコロは、やっぱり中で実行する処理のコンテキストを自分で指定できるのがよくて、globalを汚さずに済むし、時々DSLというか黒魔術っぽいこともできるあたりで、ファイル毎に色を持たせてシンプルに記述することができます。あと、vm.runInContextの第一引数には実際の処理を文字列として入れなくちゃいけなくてすげーめんどいので、runScript関数としてファイルのロード+コードのキャッシュまで全部面倒みるようにしてるのもポイント。
そしてsynchronizeは、非同期処理のはずなのに同期処理っぽく書けるようになる夢のようなモジュールです。コアにはFiberというモジュールを使っていて、このFiberというのは、Fiberのブロック内で特殊なフィールドを作り出し、イベントループは続いてるはずなのに、実際の処理はFiber.yield()で値を返さないと続きが実行されない、というよくわかんない状態になります。
laverdet/node-fibers · GitHub
FiberのReadMeにサンプルがいくつもあるので、動かしてみてくださいな。
とにかく、要はsynchronizeというのは、Fiberをもっと気軽に使えるようにするラッパーということです。
例えば、さっきのutil.jsで
sync(fs, "readFile");
とありますが、内部的にはおおよそこんな感じのことをしています。
ここでは概念的に書くだけなので、実際のコードはもう少し複雑です。
var original_readFile = fs.readFile; fs.readFile = function(filename, encode, callback) { if (typeof callback == 'function') { original_readFile.apply(fs, arguments); } else { encode = encode || "utf8"; return sync.await(original_readFile.call(fs, filename, encode, sync.defer())); } };
さてここで新たにawaitとdeferというメソッドが出て来ました。sync.deferメソッドは、大雑把に書くとこんなことをしてます。
sync.defer = function() { var fib = Fiber.current; return function(err, result) { if (err) { throw err; } fib.run(result); }; };
これを、sync.await(= Fiber.yield)で待って結果を返す、というのが、synchronizeで同期っぽく見せるカラクリです。
ここで注意なのが、sync.defer()で返ってくるコールバック関数は、必ず第一引数にエラー、第二引数に結果オブジェクト、というルールになっているので、適当に第一引数に結果を渡すような実装にsync.defer()を混ぜてしまうと、かなり泣きを見ることになります。
これを応用して、DB周りの実装でsynchronizeを使うと、ライフチェンジング度がもっとアップします。今Sequelizeを使っているのですが、モデル定義の際、classMethodsに以下の様な処理を入れてみました。
opts.classMethods = { findAwait: function(attrs) { var cb = sync.defer(); return sync.await( this.find(attrs) .success(function(obj) { cb(null, obj); }) .error(function(err) { cb(err, null); }) ); }, findAllAwait: function(attrs) { var cb = sync.defer(); return sync.await( this.findAll(attrs) .success(function(results) { cb(null, results); }) .error(function(err) { cb(err, null); }) ); } };
そうすると、今までは
User.find({where:{foo:"bar"}}).success(function(user) { // コールバックで続きの処理を書かなくてはいけない });
だったのが、
var user = User.findAwait({where:{foo:"bar"}});
と書けるようになるので、これなら同期処理の書き方と殆ど変わらない!!!ほんの2ヶ月くらい前だったらjQuery.Deferredを使ってましたが、このsynchronize(=Fiber)の旨味を知っちゃったら、もう昔には戻れないっすね。。。
もちろん、こうした書き方はsync.fiber()(というかFiber(function() {}).run())の中でしか動かないという制約はあるので、実装は多少変更にはなるのですが、その変更部分をなるべく最小限に抑えられれば、この余りあるメリットを存分に享受できるのではないでしょうか。
あと欲を言えば、Fiberかsynchronizeには、concurrencyもサポートしてもらえたら最高ですね。そうすると、RubyのEM-Synchronyっぽいことができるようになるなぁ、と。