小林 (koichik) です. > 対策としては、キャッシュデータをいきなりcache[f]につめこみ始めるのではなくて、endでcacheに移す、という手段がとれるかと思います。
もちろんそれで不具合は回避できますが、すでに 書かれているように効率を考えると嬉しくないですよね。 誰かがファイルを読み始めているなら、全員がそれを 利用したいものです。 実は! ここでは話題の Promise がピタリとはまります!! cache.content は直接 Buffer を持つのではなく、 Buffer の Promise を持つことにすると、冒頭の キャッシュをチェックするところは if(cache[f]) { cache[f].content.then(function (buf) { response.writeHead(200, headers); response.end(buf); }); return; } と書くことができます。 Promise はすでに Buffer が完成していれば即座に (おそらくは同期的に) コールバックを呼び出し、 まだなら将来 (Future、Promise の別名ですね) 完成した時にコールバックを呼び出してくれます。 とはいえ、課題のサンプルは標準モジュールしか 使っていないようなので、ここでは Promise の 代わりに普通のコールバックを使って、 if(cache[f]) { cache[f].getContent(function (err, buf) { response.writeHead(200, headers); response.end(buf); }); return; } とすることにしましょう。 慣例にならってコールバックの第 1 引数を err に してますが、エラー処理は省略します (心より恥じる)。 すると、キャッシュを作るところでは Buffer が 埋まったところで溜まってるコールバックを 呼んであげる必要があるので、'end' イベントのリスナを 登録してこんな感じにすればいいんじゃないかと。 fs.stat(f, function(err, stats) { var bufferOffset = 0; var content = new Buffer(stats.size); var complete = false; var callbacks = []; cache[f] = { getContent: function(cb) { if (complete) return cb(null, content); callbacks.push(cb); } }; s.on('data', function(data) { data.copy(cache[f].content, bufferOffset); bufferOffset += data.length; }); s.on('end', function() { complete = true; callbacks.forEach(function(cb) { cb(null, content); }); callbacks = null; }); }); ということが Promise を使うともっと簡単にできます。 うまく使えば便利であることは間違いありません。 なお、上記のコードはメーラで書いただけなので、 うまく動くかどうかはおろか、文法エラーが ないことさえ未確認です。心より恥じる。 On Tue, 16 Apr 2013 10:51:59 +0900, yuichiro wada <yuichirow...@gmail.com> wrote: > はじめまして。和田と申します。 > このスレのトピックの本の訳者です。 > > コードを見て、クックブックのコードだな、と思ったのですが、すぐにお返事できない間にここまで伸びてしまうとは…aporo4000さんの質問内容についてはすでに皆様にお答えいただいていますので、小林さんの課題に回答してみたいと思います。 > > 考えられる不具合として、あるリクエストが到着して(2)のプロセスに入って、複数回発生する可能性のある(3)が全て終了する前に別のリクエストの(1)が入る場合、不完全なcache[f]が存在しており、それがそのまま送信されてしまう可能性があります。 > > 対策としては、キャッシュデータをいきなりcache[f]につめこみ始めるのではなくて、endでcacheに移す、という手段がとれるかと思います。 > > fs.stat(f, function(err, stats) { > //(2) > var bufferOffset = 0; > var c = {content: new Buffer(stats.size)}; // cache[f]に直接バッファを作らない > s.on('data', function(data) { > //(3) > data.copy(c.content, bufferOffset); > bufferOffset += data.length; > }); > s.on('end', function(){ // end時にcache[f]に移す > cache[f] = c; > }); > }); > }).listen(8080); > > > しかしこれでは、超巨大ファイルにアクセスが殺到するとオーバーフローしそうです。そのため、効果は限定的になるかもしれませんが、// > (3)の直下にif(cache[f]) > return;などの出口を設けるか、 > 書籍の次の節で紹介しているキャッシュファイルサイズ制限を設けて、巨大なファイルはキャッシュ対象としないことにする仕組みを併せて実装するとよいかもしれません。(書籍をお買い求めいただいてもかまいませんが、サンプルコードが > http://psginc.jp/nodecookbookにあります。キャッシュファイルサイズ制限を実装したコードは1.4.1です。) > > 訳者として、書籍内容についてのご質問およびご指摘にお礼申し上げます。また、読者の皆様には至らぬ点お詫び申し上げます。 > > 和田 > > -- > yuichirow...@gmail.com (pc/mobile) > mobile 090-1736-8716 > @yuichirowada on twitter > > -- > yuichirow...@gmail.com (pc/mobile) > mobile 090-1736-8716 > @yuichirowada on twitter > > > > 2013/4/16 Koichi Kobayashi <koic...@improvement.jp> > > > 小林 (koichik) です. > > > > 勝手に課題シリーズ第二弾w > > 前のメールでも書いたように、非同期 API では > > > > ... //(1) > > foo(hoge, function(err) { > > ... //(2) > > }); > > ... //(3) > > > > の場合の処理順は (1)->(3)->(2) となります。 > > > > 問題のコードは HTTP を処理するリスナの中に > > あるので、 > > > > http.createServer(function(req, res) { > > ... //(1) > > foo(hoge, function(err) { > > ... //(2) > > }); > > ... //(3) > > }); > > > > となるわけですが、ここで二つの HTTP リクエスト、 > > A と B がほとんど同時に (ただし A が先に) 到着した > > 場合の処理順を考えてみましょう。 > > > > その場合は、 > > A(1)->A(3)->A(2)->B(1)->B(3)->B(2) となる。。。 > > 保証はなくて、 > > > > A(1)->A(3)->B(1)->B(3)->A(2)->B(2) だったり > > A(1)->A(3)->B(1)->B(3)->B(2)->A(2) だったり > > する可能性もあります。 > > リクエスト A に関する処理が全て完了してから > > リクエスト B の処理が開始されるとは限らない > > わけです。 > > > > さて、問題のコードからキャッシュの処理に着目すると、 > > > > http.createServer(function (request, response) { > > //(1) > > if(cache[f]) { > > response.writeHead(200, headers); > > response.end(cache[f].content); > > return; > > } > > > > fs.stat(f, function(err, stats) { > > //(2) > > var bufferOffset = 0; > > cache[f] = {content: new Buffer(stats.size)}; > > s.on('data', function(data) { > > //(3) > > data.copy(cache[f].content, bufferOffset); > > bufferOffset += data.length; > > }); > > }); > > }).listen(8080); > > > > という構造を抽出できるのですが、最初に到着した > > リクエスト A の処理では、 > > > > (1) キャッシュが存在しないことを確認し、 > > (2) キャッシュを作成して fs.stat() を呼び出し、 > > (3) 'data' イベントが発生するとキャッシュの中身を設定 > > > > と流れていくことはもう理解できているかと思います。 > > > > では、この途中のどこかで別のリクエスト B が到着すると > > どうなるでしょうか? > > > > A(1)->A(2)->A(3) > > > > と進む途中のどこかで、リクエスト B の処理が > > 挟まってきた場合を考えてみましょう。 > > > > A(1)->B(1)->? > > > > とか、 > > > > A(1)->A(2)->B(1)->? > > > > とか、タイミングによっては B の処理が進む経路も > > 変わってくるはずですね。 > > > > 掲載されたコードはどんな場合でも正しく > > 動作するでしょうか? > > うまくいかないのはどんな場合? > > どんな場合でもうまく動くようにするに > > どうしたらいいでしょうか? > > > > > > On Mon, 15 Apr 2013 06:18:31 -0700 (PDT), aporo4000 <hiroshi4...@gmail.com> > > wrote: > > > > > いまちょうどそれを勉強してる初心者なので、みなさんみたいにスラスラ書いたり出来るように頑張ります! > > > > > > -- > > > > > > --- > > > このメールは Google グループのグループ「Node.js 日本ユーザグループ」の登録者に送られています。 > > > このグループから退会し、メールの受信を停止するには、nodejs_jp+unsubscr...@googlegroups.comにメールを送信します。 > > > その他のオプションについては、https://groups.google.com/groups/opt_out にアクセスしてください。 > > > > > > > > > -- > > { > > name: "Koichi Kobayashi", > > mail: "koic...@improvement.jp", > > blog: "http://d.hatena.ne.jp/koichik/", > > twitter: "@koichik" > > } > > > > -- > > > > --- > > このメールは Google グループのグループ「Node.js 日本ユーザグループ」の登録者に送られています。 > > このグループから退会し、メールの受信を停止するには、nodejs_jp+unsubscr...@googlegroups.comにメールを送信します。 > > その他のオプションについては、https://groups.google.com/groups/opt_out にアクセスしてください。 > > > > > > > > -- > > --- > このメールは Google グループのグループ「Node.js 日本ユーザグループ」の登録者に送られています。 > このグループから退会し、メールの受信を停止するには、nodejs_jp+unsubscr...@googlegroups.com にメールを送信します。 > その他のオプションについては、https://groups.google.com/groups/opt_out にアクセスしてください。 > > -- { name: "Koichi Kobayashi", mail: "koic...@improvement.jp", blog: "http://d.hatena.ne.jp/koichik/", twitter: "@koichik" } -- --- このメールは Google グループのグループ「Node.js 日本ユーザグループ」の登録者に送られています。 このグループから退会し、メールの受信を停止するには、nodejs_jp+unsubscr...@googlegroups.com にメールを送信します。 その他のオプションについては、https://groups.google.com/groups/opt_out にアクセスしてください。