9
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

今さらnode-postgres案内(2) 後始末の流儀 - client.end()とclient.on 'drain'

Last updated at Posted at 2015-06-19

#なんか、1000回クエリ投げたらおかしい、という話もあったけど
PostgreSQLにテストデータを作成するnode.jsスクリプト」には、

1000件とかループすると、PostgreSQL側がクエリーを処理仕切れない内にスクリプトが終わろうとする

って書かれているけど、ほんとかいな。それって、処理を終えないうちにコールバック呼んでるやつがいるってことでしょ? 何か勘違いが無ければ……、とまで書いてから、待てよ、と読み直したら、やはり継続の使い方の誤り発見。解決策は「【保存版】制御構造別非同期プログラミング完全制覇(サーバサイドJavascript・CoffeeScript)」を読むこと。まあでも、将来にわたって、ソース中で見えている範囲のコードだけがDBアクセスするわけでもなかろうから、ファイルセーフ的に 'drain'を見て閉じるのは悪くない。

最初はasyncのバグかと思った。asyncは必要のないsetImmediateとnextTickが多いから隠れているだけで、(例えば、並行実行後の合流に取りこぼしがあっても、nextTickまでのタイムラグがあるから隠れちゃいやすい)とてもバギーだ。私のライブラリの方が、見通しが良いよ。例外処理の使い勝手は良くないけど、順次実行とwhileは便利だと思う。素性の怪しいライブラリを組み込むのが嫌なら、この記事でワンライナーも紹介しているから、参考にしてちょんまげ。

さて、DBに接続したら切断するよね。対になる作業はまとめて知っておいた方が良いもんね。前回同様、Javascriptのコードは下の方にあるよ。

先述の通り、冒頭にリンクした記事では、結局、client.on('drain',callback)というイベントハンドラで解決していた。この方法自体は大変正当で、本家のサンプルでもたいていそうしてる。テストの中には、ダミーのSELECT文を発行して、その直後にイベントハンドラをセットするような、一見クズいTIPSが使われているものもある。一見クズいけど、よくよく考えると悪くない。無意味な処理を入れるのは、コードレビューによっては貶されるかも知れないけど、この場合、いい線をついている。というのは、このclient.onのタイミング、冒頭のリンク先のやり方では、先述のバグを直しても確実ではないんだ。リンク先では、async.eachSeries内でクエリを発行して、終了後の回収コードでイベントハンドラclient.onを設定している。もし、asyncライブラリの内部で次のtickにワープしている間にdrainが発行されちゃったらどうなるかな? つまり、イベントハンドラをセットされる前に'drain'が発行されちゃうと、永遠に待つことになるってことだね。

だからこうする。イベントハンドラのセットは、最後のQuery発行と確実に同じTickで行うように、並べて記述するんだ。

        client.query querystr, (err,result) ->
          #こいつの返却値は要らない(既にDBがあればエラーが返るが……)
          if err
            console.log err
        #このdrain待ちは、queryと同時に仕込まないとりこぼす!
        client.on 'drain',->
          console.log 'caught drain'
          client.end()
          client.on 'end',->
            console.log 'call next'
            next null

こうすれば、drainを取りこぼすこともないし、逆に、はやくソケットを閉じすぎることもない。一番確実なやり方だ。特に、下のサンプルソースみたいに、「DBが未だできていないかも知れないから、とりあえずtemplate1に繋いでCREATEDBやってみる」というような処理を行う時には、必然的に必ず接続を閉じて繋ぎ直さないといけないから、大事なんだ。

これは一般的に言えることで、イベントハンドラで受け取って継続するパターンでは、イベントの原因となる処理の開始と同時にイベントハンドラの設定をすべきなんだ。これ、postgresの話じゃなくて、制御構造の話だけどね。

全サンプルソースはこちら。Javascript版も下にあるよ。

pg = require('pg')
packagejson = require('./package.json')
mydbname = packagejson.name
mypassword = 'password of your postgres'

mycreatedb = (err, config, db, next) ->
  if err
    console.log err
  else
    pg.connect config, (err,client) ->
      if err
        console.log err
      else
        querystr = 'CREATE DATABASE '+db+';'
        console.log querystr
        client.query querystr, (err,result) ->
          #こいつの返却値は要らない(既にDBがあればエラーが返るが……)
          if err
            console.log err
        #このdrain待ちは、queryと同時に仕込まないとりこぼす!
        client.on 'drain',->
          console.log 'caught drain'
          client.end()
          client.on 'end',->
            console.log 'call next'
            next null


mycreatedb null,{
  host: '/tmp'
  port: 5432
  user: 'postgres'
  password: mypassword
  database: 'template1'
}, mydbname, (err) ->
  if err
    console.log err
  else
      pg.connect {
        host: '/tmp'
        port: 5432
        user: 'postgres'
        password: mypassword
        database: mydbname
      }, (err,client) ->
        if err
          console.log err
        else
          console.log 'success!'
          querystr = "SELECT relname FROM pg_class WHERE relkind = 'r';"
          console.log querystr
          client.query querystr, (err,result) ->
            if err
              console.log err
            else
              console.log 'result!'
              for row,index in result.rows
                for key,value of row
                  console.log 'index: '+index+', key: '+key+', value: '+value
          #このdrain待ちは、queryと同時に仕込まないとりこぼす!
          client.on 'drain',->
            console.log 'caught!'
            client.end ->
              console.log 'end'
var pg = require('pg');
var packagejson = require('./package.json');

var mydbname = packagejson.name;
var mypassword = 'password for your postgres';

var mycreatedb = function(err, config, db, next) {
  if (err) {
    console.log(err);
  } else {
    pg.connect(config, function(err, client) {
      if (err) {
        console.log(err);
      } else {
        var querystr = 'CREATE DATABASE ' + db + ';';
        console.log(querystr);
        client.query(querystr, function(err, result) {
          //こいつの返却値は要らない(既にDBがあればエラーが返るが……)
          if (err) {
            console.log(err);
          }
        });
        //このdrain待ちは、queryと同時に仕込まないとりこぼす!
        client.on('drain', function() {
          console.log('caught drain');
          client.end();
          client.on('end', function() {
            console.log('call next');
            next(null);
          });
        });
      }
    });
  }
};

mycreatedb(null, {
  host: '/tmp',
  port: 5432,
  user: 'postgres',
  password: mypassword,
  database: 'template1'
}, mydbname, function(err) {
  if (err) {
    console.log(err);
  } else {
    pg.connect({
      host: '/tmp',
      port: 5432,
      user: 'postgres',
      password: mypassword,
      database: mydbname
    }, function(err, client) {
      var querystr;
      if (err) {
        console.log(err);
      } else {
        console.log('success!');
        querystr = "SELECT relname FROM pg_class WHERE relkind = 'r';";
        console.log(querystr);
        client.query(querystr, function(err, result) {
          if (err) {
            console.log(err);
          } else {
            console.log('result!');
            var ref = result.rows;
            var index, len;
            for (index = 0, len = ref.length; index < len; ++index) {
              var key, row = ref[index];
              for (key in row) {
                console.log('index: ' + index + ', key: ' + key + ', value: ' + row[key]);
              }
            }
          }
        });
        //このdrain待ちは、queryと同時に仕込まないとりこぼす!
        client.on('drain', function() {
          console.log('caught!');
          client.end(function() {
            console.log('end');
          });
        });
      }
    });
  }
});
9
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?