HDBC-MySQLのRow retrieval was canceled by mysql_stmt_close() call対策

ここでは、HaskellでMySQLを使うためのライブラリであるHDBC-MySQLを用いたときに出ることのあるエラーを回避するためのTipsをダラダラと書く。なお、技術力不足により、全容を明らかにしたわけではない。

この記事は、HDBC-mysqlを使うのが難しいの続きである。

さて、

エラーは以下である。

SqlError {seState = “”, seNativeError = 2050, seErrorMsg = “Row retrieval was canceled by mysql_stmt_close() call”}

これは、PHP+PDOではよく知られたエラーであるようだ。また、MySQLのクライアント向けライブラリであるlibmysqlclientを利用したライブラリでは共通に起こる問題らしい。

これらによると、

libmysqlclientではPrepared statementは一度生成されたら、最後の行までデータを読み切るまでは閉じることができない。しかし、GC言語はそういったlibmysqlclientの内部状態までは知らないため、Prepared statementが必要ないと感じたときにそのオブジェクトを破棄する。このときに、libmysqlclientは暗黙的にmysql_stmt_closeを呼ぶが、エラーで終了する。

ということらしい。

実際、MariaDB 5.5.35では、libmysql.cのL2248行目あたりが次のようになっていた。今回のエラーはCR_FETCH_CANCELEDである。

  if (mysql->status != MYSQL_STATUS_STATEMENT_GET_RESULT)
  {
    set_stmt_error(stmt, stmt->unbuffered_fetch_cancelled ?
                   CR_FETCH_CANCELED : CR_COMMANDS_OUT_OF_SYNC,
                   unknown_sqlstate, NULL);
    goto error;
  }

より詳しく知りたければ、この辺りを読めばよいだろう。

Haskellでは

今回、HDBC-MySQLを使った次のプログラムがエラーで落ちた。

import Database.HDBC
import Database.HDBC.MySQL

connectInfo =
  defaultMySQLConnectInfo { mysqlHost     = "localhost"
                          , mysqlUser     = "test_user"
                          , mysqlPassword = "test_password"
                          , mysqlDatabase = "test_database"
                          , mysqlUnixSocket = "/var/lib/mysql/mysql.sock"
                          }

openConnection = connectMySQL connectInfo

main = do
  conn <- openConnection

  st1 <- prepare conn "INSERT INTO test_data VALUES (1)"

  st2 <- prepare conn "SELECT 1 FROM test_data WHERE code = 7518"
  execute st2 []
  fetchAllRows' st2

  disconnect conn

なお、2つ目のSQLでは100000万件以上のデータがヒットする。

これは、次のようにfinish st1を加えただけで落ちなくなった。

import Database.HDBC
import Database.HDBC.MySQL

connectInfo =
  defaultMySQLConnectInfo { mysqlHost     = "localhost"
                          , mysqlUser     = "test_user"
                          , mysqlPassword = "test_password"
                          , mysqlDatabase = "test_database"
                          , mysqlUnixSocket = "/var/lib/mysql/mysql.sock"
                          }

openConnection = connectMySQL connectInfo

main = do
  conn <- openConnection

  st1 <- prepare conn "INSERT INTO test_data VALUES (1)"
  finish st1

  st2 <- prepare conn "SELECT 1 FROM test_data WHERE code = 7518"
  execute st2 []
  fetchAllRows' st2

  disconnect conn

2つ目のSQLの検索結果が小さい場合にも落ちないようだ。

思うに、2つ目のSQLの結果が大きいと、メモリを大量に使うためにGCが走って1つ目のPrepared statementオブジェクトが不意に解放される。その結果、libmysqlclientは暗黙的にmysql_stmt_closeを呼んでエラーとする(のだと思う)。いまいち自信が無いので、鵜呑みにしないように。

ともかく、finishによりPrepared statementを明示的に破棄してやると、エラーにはならない。

だがしかし、ライブラリの内部状態を意識しながらプログラムを書くというのは泣けるので、その辺を都合よくラップする関数を書いた。本記事の主目的はそっちである。

なお、HDBC-MySQLには、

stmt__ <- Foreign.Concurrent.newForeignPtr stmt_ (mysql_stmt_close stmt_)

という記述があり、Prepared statementが破棄されたときには、Haskellが責任をもって自前でmysql_stmt_closeを呼んでくれるはずなのでは無いかと思うんだが、どうもたぶんなぜかそうならない模様。この辺がわからない。

ここから書いた関数。

INSERTUPDATEDELETEをするための関数は、次のスタイルで書いた。

runCommand :: IConnection conn => conn -> String -> [SQLValue] -> IO Integer
runCommand conn cmd vals = do
  st   <- prepare conn cmd
  !res <- execute st vals
  finish st
  return res

読んだままである。記事を書くのに飽きてきているので、特に解説しない。

また、SELECTするときは、次のようにした。

withQuery :: IConnection conn =>
             conn -> String -> [SqlValue] -> ([[SqlValue]] -> a) -> IO a
withQuery conn sql vals fetch = do
  st   <- prepare conn sql
  execute st vals
  rows <- fetchAllRows st
  !res <- evaluate $! fetch rows
  finish st
  return res

Prepared statementがfinishされないまま関数の外に洩れ出すのを防ぎたい。一方、HDBCには結果行を遅延リストとして得る機能があり、これはHDBCの魅力の1つである。だが、遅延リストからデータを読み出す可能性が有る限り、その変数とは別にPrepared statementの変数を保持し続けなければならないため、今回の方針とは相性が悪い。

その辺をそれっぽく解決するために、上記のようになった。fetchは遅延リストから必要な分だけの行を読み込んで、好きなデータに加工して返す関数である。この読出しと加工をBangPatternを使って正格評価し、それが完了したところでPrepared statementの破棄を行い、加工済みデータを返す。

ちなみに、この関数の弱点は、fetchの中でさらなるデータベースアクセスをするコードがが書けてしまうことである。実際にやってみてはいないが、それをやると、実装方法によってはプログラムは落ちることだろう。プログラマの真の安堵はまだ手に入れられていない。これを禁止するためには、fetchの返り値をIO a型にできないようにすればよさそうだが、どうすればそんなことが可能なのかわからない。

Post a Comment

Your email is never shared.

引く

PageTop