mysqlclient-python を簡易的に非同期I/Oに対応させた

libmysqlclient を利用した PythonMySQL ドライバである libmysqlclient-python を、 gevent, meinheld など非同期I/O + 軽量スレッド環境で使いやすいようにしました。

libmysqlclient の API について

libmysqlclient は基本的に同期IOのことしか考えられていません。 ですが、クエリを投げてそのステータスを返す mysql_real_query() に限り、代替手段として mysql_send_query() でクエリを送信してから、ステータスを待つ mysql_read_query_result() を呼ぶという事ができます。 また、 MYSQL 型の net.fd というメンバから fd を取得することができます。

これらを組み合わせて、

  1. mysql_send_query() でクエリを投げる
  2. fd を取得して、利用している非同期I/Oフレームワークの機能を使って、レスポンスが届き始めるのを待つ
  3. mysql_read_query_result() でステータスを確認する
  4. (成功なら) mysql_store_result() などで結果を利用する

という流れにすれば、クエリの結果を待つ時間をブロックせずに非同期I/Oフレームワークに戻ることができます。

接続時やクエリの送信中、結果の受信中はブロッキングI/Oになりますが

  • 接続はプールすれば回数が減らせる
  • クエリの送信はソケットバッファに乗る範囲ではブロックしない
  • 結果の受信は、ステータスの受信ができた段階で MySQL はクエリを実行完了していてあとは全力で送信してくれるので、ブロッキングI/Oで受信してもあまり無駄にブロックする時間は無い

ので、クエリの実行時間を待てるだけでも十分効果はあります。

mysqlclient-python の使い方

connection クラスに、上で紹介した3つの低レベルAPIを追加し、それに加えて waiter という mysql_read_query_result() 前に呼ばれる callback を指定できるようにしました。

この waiter は同期的に呼ばれるので、非同期I/Oに greenlet を組み合わせた gevent や meinheld などで利用できます。

greenlet を使えば Tornado や asyncio でも使えるはずですが、そちらはまだ用意していません。

このブログに乗せているコードは引用を除き CC0 1.0 で提供します。